Fix error when sorting Job list by object.
[ganeti_webmgr.git] / ganeti_web / forms / virtual_machine.py
blobc672797697f91b5b84969accc0e81d8422d7a623
1 # Copyright (C) 2010 Oregon State University et al.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
18 from django import forms
19 from django.contrib.formtools.wizard.views import CookieWizardView
20 from django.conf import settings
21 from django.core.urlresolvers import reverse
22 from django.core.validators import MaxValueValidator, MinValueValidator
23 from django.db.models import Q
24 from django.forms import (Form, BooleanField, CharField, ChoiceField,
25 IntegerField, ModelChoiceField, ValidationError,
26 MultipleChoiceField, CheckboxSelectMultiple)
27 from django.http import HttpResponseRedirect
28 # Per #6579, do not change this import without discussion.
29 from django.utils import simplejson as json
30 from django.utils.translation import ugettext_lazy as _
32 from object_log.models import LogItem
33 log_action = LogItem.objects.log_action
35 from ganeti_web.backend.queries import (cluster_qs_for_user,
36 owner_qs_for_cluster)
37 from ganeti_web.backend.templates import template_to_instance
38 from ganeti_web.caps import has_cdrom2, has_balloonmem, has_sharedfile
39 from ganeti_web.constants import (EMPTY_CHOICE_FIELD, HV_DISK_TEMPLATES,
40 HV_NIC_MODES, KVM_CHOICES, HV_USB_MICE,
41 HV_SECURITY_MODELS, KVM_FLAGS,
42 HV_DISK_CACHES, MODE_CHOICES, HVM_CHOICES,
43 VM_HELP, VM_CREATE_HELP, VM_RENAME_HELP,
44 KVM_BOOT_ORDER, HVM_BOOT_ORDER)
45 from ganeti_web.fields import DataVolumeField, MACAddressField
46 from ganeti_web.models import (Cluster, ClusterUser, Node,
47 VirtualMachineTemplate, VirtualMachine)
48 from ganeti_web.utilities import (cluster_default_info, cluster_os_list,
49 get_hypervisor, hv_prettify)
50 from ganeti_web.util.client import (REPLACE_DISK_AUTO, REPLACE_DISK_PRI,
51 REPLACE_DISK_CHG, REPLACE_DISK_SECONDARY)
52 from ganeti_web.views.generic import LoginRequiredMixin
54 username_or_mtime = Q(username='') | Q(mtime__isnull=True)
57 class VirtualMachineForm(forms.ModelForm):
58 """
59 Parent class that holds all vm clean methods
60 and shared form fields.
61 """
62 memory = DataVolumeField(label=_('Memory'), min_value=100)
63 minmem = DataVolumeField(label=_('Minimum RAM (MiB)'),
64 required=True, min_value=100)
65 maxmem = DataVolumeField(label=_('Maximum RAM (MiB)'),
66 required=True, min_value=100)
68 class Meta:
69 model = VirtualMachineTemplate
71 def create_disk_fields(self, count):
72 """
73 dynamically add fields for disks
74 """
75 self.disk_fields = range(count)
76 for i in range(count):
77 disk_size = DataVolumeField(min_value=100, required=True,
78 label=_("Disk/%s Size" % i))
79 self.fields['disk_size_%s' % i] = disk_size
81 def create_nic_fields(self, count, defaults=None):
82 """
83 dynamically add fields for nics
84 """
85 self.nic_fields = range(count)
86 for i in range(count):
87 nic_mode = forms.ChoiceField(label=_('NIC/%s Mode' % i),
88 choices=HV_NIC_MODES)
89 nic_link = forms.CharField(label=_('NIC/%s Link' % i),
90 max_length=255)
91 if defaults is not None:
92 nic_link.initial = defaults['nic_link']
93 self.fields['nic_mode_%s' % i] = nic_mode
94 self.fields['nic_link_%s' % i] = nic_link
96 def clean_hostname(self):
97 data = self.cleaned_data
98 hostname = data.get('hostname')
99 cluster = data.get('cluster')
100 if hostname and cluster:
101 # Verify that this hostname is not in use for this cluster. It can
102 # only be reused when recovering a VM that failed to deploy.
104 # Recoveries are only allowed when the user is the owner of the VM
105 try:
106 vm = VirtualMachine.objects.get(cluster=cluster,
107 hostname=hostname)
109 # detect vm that failed to deploy
110 if not vm.pending_delete and vm.template is not None:
111 current_owner = vm.owner.cast()
112 if current_owner == self.owner:
113 data['vm_recovery'] = vm
114 else:
115 msg = _("Owner cannot be changed when recovering a "
116 "failed deployment")
117 self._errors["owner"] = self.error_class([msg])
118 else:
119 raise ValidationError(_("Hostname is already in use for "
120 "this cluster"))
122 except VirtualMachine.DoesNotExist:
123 # doesn't exist, no further checks needed
124 pass
126 # Spaces in hostname will always break things.
127 if ' ' in hostname:
128 self.errors["hostname"] = self.error_class(
129 ["Hostname contains illegal character"])
130 return hostname
132 def clean_vcpus(self):
133 vcpus = self.cleaned_data.get("vcpus", None)
135 if vcpus is not None and vcpus < 1:
136 self._errors["vcpus"] = self.error_class(
137 ["At least one CPU must be present"])
138 else:
139 return vcpus
141 def clean_initrd_path(self):
142 data = self.cleaned_data['initrd_path']
143 if data and not data.startswith('/') and data != 'no_initrd_path':
144 msg = u"%s." % _('This field must start with a "/"')
145 self._errors['initrd_path'] = self.error_class([msg])
146 return data
148 def clean_security_domain(self):
149 data = self.cleaned_data['security_domain']
150 security_model = self.cleaned_data['security_model']
151 msg = None
153 if data and security_model != 'user':
154 msg = u'%s.' % _(
155 'This field can not be set if Security '
156 'Mode is not set to User')
157 elif security_model == 'user':
158 if not data:
159 msg = u'%s.' % _('This field is required')
160 elif not data[0].isalpha():
161 msg = u'%s.' % _('This field must being with '
162 'an alpha character')
164 if msg:
165 self._errors['security_domain'] = self.error_class([msg])
166 return data
168 def clean_vnc_x509_path(self):
169 data = self.cleaned_data['vnc_x509_path']
170 if data and not data.startswith('/'):
171 msg = u'%s,' % _('This field must start with a "/"')
172 self._errors['vnc_x509_path'] = self.error_class([msg])
173 return data
176 def check_quota_modify(form):
177 """ method for validating user is within their quota when modifying """
178 data = form.cleaned_data
179 cluster = form.cluster
180 owner = form.owner
181 vm = form.vm
183 # check quota
184 if owner is not None:
185 start = data['start']
186 quota = cluster.get_quota(owner)
187 if quota.values():
188 used = owner.used_resources(cluster, only_running=True)
190 if (start and quota['ram'] is not None and
191 (used['ram'] + data['memory']-vm.ram) > quota['ram']):
192 del data['memory']
193 q_msg = u"%s" % _("Owner does not have enough ram "
194 "remaining on this cluster. You must "
195 "reduce the amount of ram.")
196 form._errors["ram"] = form.error_class([q_msg])
198 if 'disk_size' in data and data['disk_size']:
199 if quota['disk'] and used['disk'] + data['disk_size'] > \
200 quota['disk']:
201 del data['disk_size']
202 q_msg = u"%s" % _("Owner does not have enough diskspace "
203 "remaining on this cluster.")
204 form._errors["disk_size"] = form.error_class([q_msg])
206 if (start and quota['virtual_cpus'] is not None and
207 (used['virtual_cpus'] + data['vcpus']
208 - vm.virtual_cpus) >
209 quota['virtual_cpus']):
210 del data['vcpus']
211 q_msg = u"%s" % _("Owner does not have enough virtual "
212 "cpus remaining on this cluster. You "
213 "must reduce the amount of virtual "
214 "cpus.")
215 form._errors["vcpus"] = form.error_class([q_msg])
218 class ModifyVirtualMachineForm(VirtualMachineForm):
220 Base modify class.
221 If hvparam_fields (itirable) set on child, then
222 each field on the form will be initialized to the
223 value in vm.info.hvparams
225 always_required = ('vcpus', 'memory')
226 empty_field = EMPTY_CHOICE_FIELD
228 nic_count = forms.IntegerField(initial=1, widget=forms.HiddenInput())
229 os = forms.ChoiceField(label=_('Operating System'), choices=[empty_field])
231 class Meta:
232 model = VirtualMachineTemplate
233 exclude = ('start', 'owner', 'cluster', 'hostname', 'name_check',
234 'iallocator', 'iallocator_hostname', 'disk_template',
235 'pnode', 'nics', 'snode', 'disk_size', 'nic_mode',
236 'template_name', 'hypervisor', 'disks', 'description',
237 'no_install', 'ip_check', 'temporary')
239 def __init__(self, vm, initial=None, *args, **kwargs):
240 super(VirtualMachineForm, self).__init__(initial, *args, **kwargs)
242 if has_balloonmem(vm.cluster):
243 self.always_required = ('vcpus', 'memory', 'minmem')
245 # Set owner on form
246 try:
247 self.owner
248 except AttributeError:
249 self.owner = vm.owner
251 # Setup os choices
252 os_list = cluster_os_list(vm.cluster)
253 self.fields['os'].choices = os_list
255 for field in self.always_required:
256 self.fields[field].required = True
257 # If the required property is set on a child class,
258 # require those form fields
259 try:
260 if self.required:
261 for field in self.required:
262 self.fields[field].required = True
263 except AttributeError:
264 pass
266 # Need to set initial values from vm.info as these are not saved
267 # per the vm model.
268 if vm.info:
269 info = vm.info
270 hvparam = info['hvparams']
271 # XXX Convert ram string since it comes out
272 # from ganeti as an int and the DataVolumeField does not like
273 # ints.
274 self.fields['vcpus'].initial = info['beparams']['vcpus']
275 if has_balloonmem(vm.cluster):
276 del self.fields['memory']
277 self.fields['minmem'].initial = info['beparams']['minmem']
278 self.fields['maxmem'].initial = info['beparams']['maxmem']
279 else:
280 del self.fields['minmem']
281 del self.fields['maxmem']
282 self.fields['memory'].initial = str(info['beparams']['memory'])
284 # always take the larger nic count. this ensures that if nics are
285 # being removed that they will be in the form as Nones
286 self.nics = len(info['nic.links'])
287 nic_count = int(initial.get('nic_count', 1)) if initial else 1
288 nic_count = self.nics if self.nics > nic_count else nic_count
289 self.fields['nic_count'].initial = nic_count
290 self.nic_fields = xrange(nic_count)
291 for i in xrange(nic_count):
292 link = forms.CharField(label=_('NIC/%s Link' % i),
293 max_length=255, required=True)
294 self.fields['nic_link_%s' % i] = link
295 mac = MACAddressField(label=_('NIC/%s Mac' % i), required=True)
296 self.fields['nic_mac_%s' % i] = mac
297 if i < self.nics:
298 mac.initial = info['nic.macs'][i]
299 link.initial = info['nic.links'][i]
301 self.fields['os'].initial = info['os']
303 try:
304 if self.hvparam_fields:
305 for field in self.hvparam_fields:
306 self.fields[field].initial = hvparam.get(field)
307 except AttributeError:
308 pass
310 def clean(self):
311 data = self.cleaned_data
312 kernel_path = data.get('kernel_path')
313 initrd_path = data.get('initrd_path')
315 # Make sure if initrd_path is set, kernel_path is aswell
316 if initrd_path and not kernel_path:
317 msg = u"%s." % _("Kernel Path must be specified along "
318 "with Initrd Path")
319 self._errors['kernel_path'] = self.error_class([msg])
320 self._errors['initrd_path'] = self.error_class([msg])
321 del data['initrd_path']
323 vnc_tls = data.get('vnc_tls')
324 vnc_x509_path = data.get('vnc_x509_path')
325 vnc_x509_verify = data.get('vnc_x509_verify')
327 if not vnc_tls and vnc_x509_path:
328 msg = u'%s.' % _("This field can not be set without "
329 "VNC TLS enabled")
330 self._errors['vnc_x509_path'] = self.error_class([msg])
331 if vnc_x509_verify and not vnc_x509_path:
332 msg = u'%s.' % _('This field is required')
333 self._errors['vnc_x509_path'] = self.error_class([msg])
335 if self.owner:
336 data['start'] = 'reboot' in self.data or self.vm.is_running
337 check_quota_modify(self)
338 del data['start']
340 for i in xrange(data['nic_count']):
341 mac_field = 'nic_mac_%s' % i
342 link_field = 'nic_link_%s' % i
343 mac = data[mac_field] if mac_field in data else None
344 link = data[link_field] if link_field in data else None
345 if mac and not link:
346 self._errors[link_field] = self.error_class([_('This field is'
347 ' required')])
348 elif link and not mac:
349 self._errors[mac_field] = self.error_class([_('This field is '
350 'required')])
351 data['nic_count_original'] = self.nics
353 return data
356 class HvmModifyVirtualMachineForm(ModifyVirtualMachineForm):
357 hvparam_fields = ('boot_order', 'cdrom_image_path', 'nic_type',
358 'disk_type', 'vnc_bind_address', 'acpi',
359 'use_localtime')
360 required = ('disk_type', 'boot_order', 'nic_type')
361 empty_field = EMPTY_CHOICE_FIELD
362 disk_types = HVM_CHOICES['disk_type']
363 nic_types = HVM_CHOICES['nic_type']
364 boot_devices = HVM_CHOICES['boot_order']
366 acpi = forms.BooleanField(label='ACPI', required=False)
367 use_localtime = forms.BooleanField(label='Use Localtime', required=False)
368 vnc_bind_address = forms.IPAddressField(label='VNC Bind Address',
369 required=False)
370 disk_type = forms.ChoiceField(label=_('Disk Type'), choices=disk_types)
371 nic_type = forms.ChoiceField(label=_('NIC Type'), choices=nic_types)
372 boot_order = forms.ChoiceField(label=_('Boot Device'),
373 choices=boot_devices)
375 class Meta(ModifyVirtualMachineForm.Meta):
376 exclude = ModifyVirtualMachineForm.Meta.exclude + \
377 ('kernel_path', 'root_path', 'kernel_args',
378 'serial_console', 'cdrom2_image_path')
380 def __init__(self, vm, *args, **kwargs):
381 super(HvmModifyVirtualMachineForm, self).__init__(vm, *args, **kwargs)
384 class PvmModifyVirtualMachineForm(ModifyVirtualMachineForm):
385 hvparam_fields = ('root_path', 'kernel_path', 'kernel_args',
386 'initrd_path')
388 initrd_path = forms.CharField(label='initrd Path', required=False)
389 kernel_args = forms.CharField(label='Kernel Args', required=False)
391 class Meta(ModifyVirtualMachineForm.Meta):
392 exclude = ModifyVirtualMachineForm.Meta.exclude + \
393 ('disk_type', 'nic_type', 'boot_order',
394 'cdrom_image_path', 'serial_console',
395 'cdrom2_image_path')
397 def __init__(self, vm, *args, **kwargs):
398 super(PvmModifyVirtualMachineForm, self).__init__(vm, *args, **kwargs)
401 class KvmModifyVirtualMachineForm(PvmModifyVirtualMachineForm,
402 HvmModifyVirtualMachineForm):
403 hvparam_fields = (
404 'acpi', 'disk_cache', 'initrd_path',
405 'kernel_args', 'kvm_flag', 'mem_path',
406 'migration_downtime', 'security_domain',
407 'security_model', 'usb_mouse', 'use_chroot',
408 'use_localtime', 'vnc_bind_address', 'vnc_tls',
409 'vnc_x509_path', 'vnc_x509_verify', 'disk_type',
410 'boot_order', 'nic_type', 'root_path',
411 'kernel_path', 'serial_console',
412 'cdrom_image_path',
413 'cdrom2_image_path',
415 disk_caches = HV_DISK_CACHES
416 kvm_flags = KVM_FLAGS
417 security_models = HV_SECURITY_MODELS
418 usb_mice = HV_USB_MICE
419 disk_types = KVM_CHOICES['disk_type']
420 nic_types = KVM_CHOICES['nic_type']
421 boot_devices = KVM_CHOICES['boot_order']
423 disk_cache = forms.ChoiceField(label='Disk Cache', required=False,
424 choices=disk_caches)
425 kvm_flag = forms.ChoiceField(label='KVM Flag', required=False,
426 choices=kvm_flags)
427 mem_path = forms.CharField(label='Mem Path', required=False)
428 migration_downtime = forms.IntegerField(label='Migration Downtime',
429 required=False)
430 security_model = forms.ChoiceField(label='Security Model',
431 required=False, choices=security_models)
432 security_domain = forms.CharField(label='Security Domain', required=False)
433 usb_mouse = forms.ChoiceField(label='USB Mouse', required=False,
434 choices=usb_mice)
435 use_chroot = forms.BooleanField(label='Use Chroot', required=False)
436 vnc_tls = forms.BooleanField(label='VNC TLS', required=False)
437 vnc_x509_path = forms.CharField(label='VNC x509 Path', required=False)
438 vnc_x509_verify = forms.BooleanField(label='VNC x509 Verify',
439 required=False)
441 class Meta(ModifyVirtualMachineForm.Meta):
442 pass
444 def __init__(self, vm, *args, **kwargs):
445 super(KvmModifyVirtualMachineForm, self).__init__(vm, *args, **kwargs)
446 self.fields['disk_type'].choices = self.disk_types
447 self.fields['nic_type'].choices = self.nic_types
448 self.fields['boot_order'].choices = self.boot_devices
451 class ModifyConfirmForm(forms.Form):
453 def clean(self):
454 raw = self.data['rapi_dict']
455 data = json.loads(raw)
457 cleaned = self.cleaned_data
458 cleaned['rapi_dict'] = data
460 # XXX copy properties into cleaned data so that check_quota_modify can
461 # be used
462 if data.get('maxmem'):
463 cleaned['maxmem'] = data['maxmem']
464 cleaned['minmem'] = data['minmem']
465 else:
466 cleaned['memory'] = data['memory']
467 cleaned['vcpus'] = data['vcpus']
468 cleaned['start'] = 'reboot' in data or self.vm.is_running
469 check_quota_modify(self)
471 # Build NICs dicts. Add changes for existing nics and mark new or
472 # removed nics
474 # XXX Ganeti only allows a single remove or add but this code will
475 # format properly for unlimited adds or removes in the hope that this
476 # limitation is removed sometime in the future.
477 nics = []
478 nic_count_original = data.pop('nic_count_original')
479 nic_count = data.pop('nic_count')
480 for i in xrange(nic_count):
481 nic = dict(link=data.pop('nic_link_%s' % i))
482 if 'nic_mac_%s' % i in data:
483 nic['mac'] = data.pop('nic_mac_%s' % i)
484 index = i if i < nic_count_original else 'add'
485 nics.append((index, nic))
486 for i in xrange(nic_count_original-nic_count):
487 nics.append(('remove', {}))
488 try:
489 del data['nic_mac_%s' % (nic_count+i)]
490 except KeyError:
491 pass
492 del data['nic_link_%s' % (nic_count+i)]
494 data['nics'] = nics
495 return cleaned
498 class MigrateForm(forms.Form):
499 """ Form used for migrating a Virtual Machine """
500 mode = forms.ChoiceField(choices=MODE_CHOICES)
501 cleanup = forms.BooleanField(initial=False, required=False,
502 label=_("Attempt recovery from failed "
503 "migration"))
506 class RenameForm(forms.Form):
507 """ form used for renaming a Virtual Machine """
508 hostname = forms.CharField(label=_('Instance Name'), max_length=255,
509 required=True)
510 ip_check = forms.BooleanField(initial=True, required=False,
511 label=_('IP Check'))
512 name_check = forms.BooleanField(initial=True, required=False,
513 label=_('DNS Name Check'))
515 def __init__(self, vm, *args, **kwargs):
516 self.vm = vm
517 super(RenameForm, self).__init__(*args, **kwargs)
519 def clean_hostname(self):
520 data = self.cleaned_data
521 hostname = data.get('hostname', None)
522 if hostname and hostname == self.vm.hostname:
523 raise ValidationError(_("The new hostname must be different than "
524 "the current hostname"))
525 return hostname
528 class ChangeOwnerForm(forms.Form):
529 """ Form used when modifying the owner of a virtual machine """
530 owner = forms.ModelChoiceField(queryset=ClusterUser.objects.all(),
531 label=_('Owner'))
534 class ReplaceDisksForm(forms.Form):
536 Form used when replacing disks for a virtual machine
538 empty_field = EMPTY_CHOICE_FIELD
540 MODE_CHOICES = (
541 (REPLACE_DISK_AUTO, _('Automatic')),
542 (REPLACE_DISK_PRI, _('Replace disks on primary')),
543 (REPLACE_DISK_SECONDARY, _('Replace disks secondary')),
544 (REPLACE_DISK_CHG, _('Replace secondary with new disk')),
547 mode = forms.ChoiceField(choices=MODE_CHOICES, label=_('Mode'))
548 disks = forms.MultipleChoiceField(label=_('Disks'), required=False)
549 node = forms.ChoiceField(label=_('Node'), choices=[empty_field],
550 required=False)
551 iallocator = forms.BooleanField(initial=False, label=_('Iallocator'),
552 required=False,
553 help_text=_(VM_CREATE_HELP['iallocator']))
555 def __init__(self, instance, *args, **kwargs):
556 super(ReplaceDisksForm, self).__init__(*args, **kwargs)
557 self.instance = instance
559 # set disk choices based on the instance
560 disk_choices = [(i, 'disk/%s' % i) for i, v in
561 enumerate(instance.info['disk.sizes'])]
562 self.fields['disks'].choices = disk_choices
564 # set choices based on the instances cluster
565 cluster = instance.cluster
566 nodelist = [str(h) for h in
567 cluster.nodes.values_list('hostname', flat=True)]
568 nodes = zip(nodelist, nodelist)
569 nodes.insert(0, self.empty_field)
570 self.fields['node'].choices = nodes
572 defaults = cluster_default_info(cluster, get_hypervisor(instance))
573 if defaults['iallocator'] != '':
574 self.fields['iallocator'].initial = True
575 self.fields['iallocator_hostname'] = forms.CharField(
576 initial=defaults['iallocator'],
577 required=False,
578 widget=forms.HiddenInput())
580 def clean(self):
581 data = self.cleaned_data
582 mode = data.get('mode')
583 if mode == REPLACE_DISK_CHG:
584 iallocator = data.get('iallocator')
585 node = data.get('node')
586 if not (iallocator or node):
587 msg = _('Node or iallocator is required when '
588 'replacing secondary with new disk')
589 self._errors['mode'] = self.error_class([msg])
591 elif iallocator and node:
592 msg = _('Choose either node or iallocator')
593 self._errors['mode'] = self.error_class([msg])
595 return data
597 def clean_disks(self):
598 """ format disks into a comma delimited string """
599 disks = self.cleaned_data.get('disks')
600 if disks is not None:
601 disks = ','.join(disks)
602 return disks
604 def clean_node(self):
605 node = self.cleaned_data.get('node')
606 return node if node else None
608 def save(self):
610 Start a replace disks job using the data in this form.
612 data = self.cleaned_data
613 mode = data['mode']
614 disks = data['disks']
615 node = data['node']
616 if data['iallocator']:
617 iallocator = data['iallocator_hostname']
618 else:
619 iallocator = None
620 return self.instance.replace_disks(mode, disks, node, iallocator)
623 class VMWizardClusterForm(Form):
624 cluster = ModelChoiceField(label=_('Cluster'),
625 queryset=Cluster.objects.all(),
626 empty_label=None)
628 class Media:
629 css = {
630 # I'm not quite sure if this is the proper way to use static
631 'all': ('/static/css/vm_wizard/cluster_form.css',)
634 def __init__(self, options=None, *args, **kwargs):
635 super(VMWizardClusterForm, self).__init__(*args, **kwargs)
636 if options:
637 self.fields['choices'] = MultipleChoiceField(
638 widget=CheckboxSelectMultiple,
639 choices=options,
640 initial=self.initial,
641 label=_('What would you '
642 'like to create?'),
643 help_text=_(VM_CREATE_HELP['choices']))
645 def _configure_for_user(self, user):
646 self.fields["cluster"].queryset = cluster_qs_for_user(user)
648 def clean_cluster(self):
650 Ensure that the cluster is available.
653 cluster = self.cleaned_data.get('cluster', None)
654 if not getattr(cluster, "info", None):
655 msg = _("This cluster is currently unavailable. Please check"
656 " for Errors on the cluster detail page.")
657 self._errors['cluster'] = self.error_class([msg])
659 return cluster
662 class VMWizardOwnerForm(Form):
663 owner = ModelChoiceField(label=_('Owner'),
664 queryset=ClusterUser.objects.all(),
665 empty_label=None,
666 help_text=_(VM_CREATE_HELP['owner']))
667 template_name = CharField(label=_("Template Name"), max_length=255,
668 required=False,
669 help_text=_(VM_HELP['template_name']))
670 hostname = CharField(label=_('Instance Name'), max_length=255,
671 required=False,
672 help_text=_(VM_CREATE_HELP['hostname']))
674 def _configure_for_cluster(self, cluster):
675 if not cluster:
676 return
678 self.cluster = cluster
680 qs = owner_qs_for_cluster(cluster)
681 self.fields["owner"].queryset = qs
683 def _configure_for_template(self, template, choices=None):
684 # for each option not checked on step 0
685 if choices:
686 for field in choices:
687 # Hide it
688 self.fields[field].widget = forms.HiddenInput()
690 if not template:
691 return
693 self.fields["template_name"].initial = template.template_name
695 def clean_hostname(self):
696 hostname = self.cleaned_data.get('hostname')
697 if hostname:
698 # Confirm that the hostname is not already in use.
699 try:
700 vm = VirtualMachine.objects.get(cluster=self.cluster,
701 hostname=hostname)
702 except VirtualMachine.DoesNotExist:
703 # Well, *I'm* convinced.
704 pass
705 else:
706 raise ValidationError(
707 _("Hostname is already in use for this cluster"))
709 # Spaces in hostname will always break things.
710 if ' ' in hostname:
711 self.errors["hostname"] = self.error_class(
712 ["Hostnames cannot contain spaces."])
713 return hostname
715 def clean(self):
716 if (not self.cleaned_data.get("template_name") and
717 not self.cleaned_data.get("hostname")):
718 raise ValidationError("What should be created?")
719 return self.cleaned_data
722 class VMWizardBasicsForm(Form):
723 hv = ChoiceField(label=_("Hypervisor"), choices=[],
724 help_text=_(VM_CREATE_HELP['hypervisor']))
725 os = ChoiceField(label=_('Operating System'), choices=[],
726 help_text=_(VM_CREATE_HELP['os']))
727 vcpus = IntegerField(label=_("Virtual CPU Count"), initial=1, min_value=1,
728 help_text=_(VM_HELP['vcpus']))
729 minram = DataVolumeField(label=_('Minimum RAM (MiB)'),
730 help_text=_(VM_HELP['memory']))
731 memory = DataVolumeField(label=_('Maximum RAM (MiB)'),
732 help_text=_(VM_HELP['memory']))
733 disk_template = ChoiceField(label=_('Disk Template'),
734 choices=HV_DISK_TEMPLATES,
735 help_text=_(VM_CREATE_HELP['disk_template']))
737 def __init__(self, *args, **kwargs):
738 super(VMWizardBasicsForm, self).__init__(*args, **kwargs)
740 # Create disk and nic fields based on value in settings
741 disk_count = settings.MAX_DISKS_ADD
742 self.create_disk_fields(disk_count)
744 nic_count = settings.MAX_NICS_ADD
745 self.create_nic_fields(nic_count)
747 def create_disk_fields(self, count):
749 dynamically add fields for disks
751 for i in range(count):
752 disk_size = DataVolumeField(
753 label=_("Disk/%s Size (MB)" % i), required=False,
754 help_text=_(VM_CREATE_HELP['disk_size']))
756 disk_size.widget.attrs['class'] = 'multi disk'
757 disk_size.widget.attrs['data-group'] = i
758 self.fields['disk_size_%s' % i] = disk_size
760 def create_nic_fields(self, count):
762 dynamically add fields for nics
764 self.nic_fields = range(count)
765 for i in range(count):
766 nic_mode = forms.ChoiceField(
767 label=_('NIC/%s Mode' % i), choices=HV_NIC_MODES, initial='',
768 required=False, help_text=_(VM_CREATE_HELP['nic_mode']))
770 nic_link = forms.CharField(
771 label=_('NIC/%s Link' % i), max_length=255, required=False,
772 initial='', help_text=_(VM_HELP['nic_link']))
774 nic_mode.widget.attrs['class'] = 'multi nic mode'
775 nic_mode.widget.attrs['data-group'] = i
776 nic_link.widget.attrs['class'] = 'multi nic link'
777 nic_link.widget.attrs['data-group'] = i
779 self.fields['nic_mode_%s' % i] = nic_mode
780 self.fields['nic_link_%s' % i] = nic_link
782 def _configure_for_cluster(self, cluster):
783 if not cluster:
784 return
786 self.cluster = cluster
788 # Get a look at the list of available hypervisors, and set the initial
789 # hypervisor appropriately.
790 hvs = cluster.info["enabled_hypervisors"]
791 prettified = [hv_prettify(hv) for hv in hvs]
792 hv = cluster.info["default_hypervisor"]
793 self.fields["hv"].choices = zip(hvs, prettified)
794 self.fields["hv"].initial = hv
796 if not has_sharedfile(cluster):
797 self.fields["disk_template"].choices.remove((u'sharedfile',
798 u'Sharedfile'))
800 # Get the OS list.
801 self.fields["os"].choices = cluster_os_list(cluster)
803 # Set the default CPU count based on the backend parameters.
804 beparams = cluster.info["beparams"]["default"]
805 self.fields["vcpus"].initial = beparams["vcpus"]
807 # Check for memory based on ganeti version
808 if has_balloonmem(cluster):
809 self.fields["memory"].initial = beparams["maxmem"]
810 self.fields["minram"].initial = beparams["minmem"]
811 else:
812 self.fields["memory"].initial = beparams["memeory"]
814 # If there are ipolicy limits in place, add validators for them.
815 if "ipolicy" in cluster.info:
816 if "max" in cluster.info["ipolicy"]:
817 v = cluster.info["ipolicy"]["max"]["disk-size"]
818 for disk in xrange(settings.MAX_DISKS_ADD):
819 self.fields["disk_size_%s" % disk].validators.append(
820 MaxValueValidator(v))
821 v = cluster.info["ipolicy"]["max"]["memory-size"]
822 self.fields["memory"].validators.append(MaxValueValidator(v))
823 if has_balloonmem(cluster):
824 self.fields["minram"].validators.append(
825 MaxValueValidator(v))
826 if "min" in cluster.info["ipolicy"]:
827 v = cluster.info["ipolicy"]["min"]["disk-size"]
828 for disk in xrange(settings.MAX_DISKS_ADD):
829 self.fields["disk_size_%s" % disk].validators.append(
830 MinValueValidator(v))
831 v = cluster.info["ipolicy"]["min"]["memory-size"]
832 self.fields["memory"].validators.append(MinValueValidator(v))
833 if has_balloonmem(cluster):
834 self.fields["minram"].validators.append(
835 MinValueValidator(v))
837 def _configure_for_template(self, template):
838 if not template:
839 return
841 self.fields["os"].initial = template.os
842 self.fields["vcpus"].initial = template.vcpus
843 self.fields["memory"].initial = template.memory
844 if has_balloonmem(cluster):
845 self.fields["minram"].initial = template.minmem
846 self.fields["disk_template"].initial = template.disk_template
847 for num, disk in enumerate(template.disks):
848 self.fields["disk_size_%s" % num].initial = disk["size"]
849 for num, nic in enumerate(template.nics):
850 self.fields["nic_link_%s" % num].initial = nic['link']
851 self.fields["nic_mode_%s" % num].initial = nic['mode']
853 def clean(self):
854 data = self.cleaned_data
855 # get disk sizes after validation (after 1.5G -> 1500)
856 # and filter empty fields.
857 disks = []
858 for disk_num in xrange(settings.MAX_DISKS_ADD):
859 disk = data.get("disk_size_%s" % disk_num, None)
860 if disk:
861 disks.append(disk)
862 # if disks validated (no errors), but none of them contain data, then
863 # they were all left empty
864 if not disks and not self._errors:
865 msg = _("You need to add at least 1 disk!")
866 self._errors["disk_size_0"] = self.error_class([msg])
868 # Store disks as an array of dicts for use in template.
869 data["disks"] = [{"size": disk} for disk in disks]
871 nics = []
872 for nic in xrange(settings.MAX_NICS_ADD):
873 link = data.get('nic_link_%s' % nic, None)
874 mode = data.get('nic_mode_%s' % nic, None)
875 # if both the mode and link for a NIC are filled out, add it to the
876 # nic list.
877 if link and mode:
878 nics.append({'link': link, 'mode': mode})
879 elif link or mode:
880 raise ValidationError(_("Please input both a link and mode."))
882 data['nics'] = nics
884 if data.get('minram') > data.get('memory'):
885 msg = _("The minimum ram cannot be larger than the maximum ram.")
886 self._errors["minram"] = self.error_class([msg])
888 return data
891 class VMWizardAdvancedForm(Form):
892 ip_check = BooleanField(label=_('Verify IP'), initial=False,
893 required=False,
894 help_text=_(VM_RENAME_HELP['ip_check']))
895 name_check = BooleanField(label=_('Verify hostname through DNS'),
896 initial=False, required=False,
897 help_text=_(VM_RENAME_HELP['name_check']))
898 pnode = ModelChoiceField(label=_("Primary Node"),
899 queryset=Node.objects.all(), empty_label=None,
900 help_text=_(VM_CREATE_HELP['pnode']))
901 snode = ModelChoiceField(label=_("Secondary Node"),
902 queryset=Node.objects.all(), empty_label=None,
903 help_text=_(VM_CREATE_HELP['snode']))
905 def _configure_for_cluster(self, cluster):
906 if not cluster:
907 return
909 self.cluster = cluster
911 qs = Node.objects.filter(cluster=cluster)
912 self.fields["pnode"].queryset = qs
913 self.fields["snode"].queryset = qs
915 def _configure_for_template(self, template):
916 if not template:
917 return
919 self.fields["ip_check"].initial = template.ip_check
920 self.fields["name_check"].initial = template.name_check
921 self.fields["pnode"].initial = template.pnode
922 self.fields["snode"].initial = template.snode
924 def _configure_for_disk_template(self, template):
925 if template != "drbd":
926 del self.fields["snode"]
928 def clean(self):
929 # Ganeti will error on VM creation if an IP address check is requested
930 # but a name check is not.
931 if (self.cleaned_data.get("ip_check") and not
932 self.cleaned_data.get("name_check")):
933 msg = ["Cannot perform IP check without name check"]
934 self.errors["ip_check"] = self.error_class(msg)
936 return self.cleaned_data
939 class VMWizardPVMForm(Form):
940 kernel_path = CharField(label=_("Kernel path"), max_length=255)
941 root_path = CharField(label=_("Root path"), max_length=255)
943 def _configure_for_cluster(self, cluster):
944 if not cluster:
945 return
947 self.cluster = cluster
948 params = cluster.info["hvparams"]["xen-pvm"]
950 self.fields["kernel_path"].initial = params["kernel_path"]
951 self.fields["root_path"].initial = params["root_path"]
953 def _configure_for_template(self, template):
954 if not template:
955 return
957 self.fields["kernel_path"].initial = template.kernel_path
958 self.fields["root_path"].initial = template.root_path
961 class VMWizardHVMForm(Form):
962 boot_order = ChoiceField(label=_("Preferred boot device"),
963 required=False, choices=HVM_BOOT_ORDER,
964 help_text=_(VM_CREATE_HELP['boot_order']))
965 cdrom_image_path = CharField(label=_("CD-ROM image path"), max_length=512,
966 required=False,
967 help_text=_(
968 VM_CREATE_HELP['cdrom_image_path']))
969 disk_type = ChoiceField(label=_("Disk type"),
970 choices=HVM_CHOICES["disk_type"],
971 help_text=_(VM_CREATE_HELP['disk_type']))
972 nic_type = ChoiceField(label=_("NIC type"),
973 choices=HVM_CHOICES["nic_type"],
974 help_text=_(VM_CREATE_HELP['nic_type']))
976 def _configure_for_cluster(self, cluster):
977 if not cluster:
978 return
980 self.cluster = cluster
981 params = cluster.info["hvparams"]["xen-pvm"]
983 self.fields["boot_order"].initial = params["boot_order"]
984 self.fields["disk_type"].initial = params["disk_type"]
985 self.fields["nic_type"].initial = params["nic_type"]
987 def _configure_for_template(self, template):
988 if not template:
989 return
991 self.fields["boot_order"].initial = template.boot_order
992 self.fields["cdrom_image_path"].initial = template.cdrom_image_path
993 self.fields["disk_type"].initial = template.disk_type
994 self.fields["nic_type"].initial = template.nic_type
997 class VMWizardKVMForm(Form):
998 kernel_path = CharField(label=_("Kernel path"), max_length=255,
999 help_text=_(VM_CREATE_HELP['kernel_path']))
1000 root_path = CharField(label=_("Root path"), max_length=255,
1001 help_text=_(VM_CREATE_HELP['root_path']))
1002 serial_console = BooleanField(label=_("Enable serial console"),
1003 required=False,
1004 help_text=_(
1005 VM_CREATE_HELP['serial_console']))
1006 boot_order = ChoiceField(label=_("Preferred boot device"),
1007 required=False, choices=KVM_BOOT_ORDER,
1008 help_text=_(VM_CREATE_HELP['boot_order']))
1009 cdrom_image_path = CharField(label=_("CD-ROM image path"), max_length=512,
1010 required=False,
1011 help_text=_(
1012 VM_CREATE_HELP['cdrom_image_path']))
1013 cdrom2_image_path = CharField(label=_("Second CD-ROM image path"),
1014 max_length=512, required=False,
1015 help_text=_(
1016 VM_CREATE_HELP['cdrom2_image_path']))
1017 disk_type = ChoiceField(label=_("Disk type"),
1018 choices=KVM_CHOICES["disk_type"],
1019 help_text=_(VM_CREATE_HELP['disk_type']))
1020 nic_type = ChoiceField(label=_("NIC type"),
1021 choices=KVM_CHOICES["nic_type"],
1022 help_text=_(VM_CREATE_HELP['nic_type']))
1024 def _configure_for_cluster(self, cluster):
1025 if not cluster:
1026 return
1028 self.cluster = cluster
1029 params = cluster.info["hvparams"]["kvm"]
1031 self.fields["boot_order"].initial = params["boot_order"]
1032 self.fields["disk_type"].initial = params["disk_type"]
1033 self.fields["kernel_path"].initial = params["kernel_path"]
1034 self.fields["nic_type"].initial = params["nic_type"]
1035 self.fields["root_path"].initial = params["root_path"]
1036 self.fields["serial_console"].initial = params["serial_console"]
1038 # Remove cdrom2 if the cluster doesn't have it; see #11655.
1039 if not has_cdrom2(cluster):
1040 del self.fields["cdrom2_image_path"]
1042 def _configure_for_template(self, template):
1043 if not template:
1044 return
1046 self.fields["kernel_path"].initial = template.kernel_path
1047 self.fields["root_path"].initial = template.root_path
1048 self.fields["serial_console"].initial = template.serial_console
1049 self.fields["boot_order"].initial = template.boot_order
1050 self.fields["cdrom_image_path"].initial = template.cdrom_image_path
1051 self.fields["cdrom2_image_path"].initial = template.cdrom2_image_path
1052 self.fields["disk_type"].initial = template.disk_type
1053 self.fields["nic_type"].initial = template.nic_type
1055 def clean(self):
1056 data = super(VMWizardKVMForm, self).clean()
1058 # Force cdrom disk type to IDE; see #9297.
1059 data['cdrom_disk_type'] = 'ide'
1061 # If booting from CD-ROM, require the first CD-ROM image to be
1062 # present.
1063 if (data.get("boot_order") == "cdrom" and
1064 not data.get("cdrom_image_path")):
1065 msg = u"%s." % _("Image path required if boot device is CD-ROM")
1066 self._errors["cdrom_image_path"] = self.error_class([msg])
1068 return data
1071 class VMWizardView(LoginRequiredMixin, CookieWizardView):
1072 template_name = "ganeti/forms/vm_wizard.html"
1074 OPTIONS = (
1075 # value, display value
1076 # value corresponds to VMWizardOwnerForm's fields
1077 ('template_name', 'Template'),
1078 ('hostname', 'Virtual Machine'),
1081 def _get_vm_or_template(self):
1082 """Returns items that were not checked in step0"""
1083 data = self.get_cleaned_data_for_step('0')
1084 if data:
1085 options = [option[0] for option in self.OPTIONS]
1086 choices = data.get('choices', None)
1087 # which boxes weren't checked
1088 unchecked = set(options) - set(choices)
1089 return unchecked
1091 return None
1093 def _get_template(self):
1094 name = self.kwargs.get("template")
1095 if name:
1096 return VirtualMachineTemplate.objects.get(template_name=name)
1097 return None
1099 def _get_cluster(self):
1100 data = self.get_cleaned_data_for_step("0")
1101 if data:
1102 return data["cluster"]
1103 return None
1105 def _get_hv(self):
1106 data = self.get_cleaned_data_for_step("2")
1107 if data:
1108 return data["hv"]
1109 return None
1111 def _get_disk_template(self):
1112 data = self.get_cleaned_data_for_step("2")
1113 if data:
1114 return data["disk_template"]
1115 return None
1117 def get_form(self, step=None, data=None, files=None):
1118 s = int(self.steps.current) if step is None else int(step)
1119 initial = self.get_form_initial(s)
1121 if s == 0:
1122 form = VMWizardClusterForm(data=data, options=self.OPTIONS,
1123 initial=initial)
1124 form._configure_for_user(self.request.user)
1125 # XXX this should somehow become totally invalid if the user
1126 # doesn't have perms on the template.
1127 elif s == 1:
1128 form = VMWizardOwnerForm(data=data)
1129 form._configure_for_cluster(self._get_cluster())
1130 form._configure_for_template(self._get_template(),
1131 choices=self._get_vm_or_template())
1132 elif s == 2:
1133 form = VMWizardBasicsForm(data=data)
1134 form._configure_for_cluster(self._get_cluster())
1135 form._configure_for_template(self._get_template())
1136 elif s == 3:
1137 form = VMWizardAdvancedForm(data=data)
1138 form._configure_for_cluster(self._get_cluster())
1139 form._configure_for_template(self._get_template())
1140 form._configure_for_disk_template(self._get_disk_template())
1141 elif s == 4:
1142 cluster = self._get_cluster()
1143 hv = self._get_hv()
1144 form = None
1146 if cluster and hv:
1147 if hv == "kvm":
1148 form = VMWizardKVMForm(data=data)
1149 elif hv == "xen-pvm":
1150 form = VMWizardPVMForm(data=data)
1151 elif hv == "xen-hvm":
1152 form = VMWizardHVMForm(data=data)
1154 if form:
1155 form._configure_for_cluster(cluster)
1156 form._configure_for_template(self._get_template())
1157 else:
1158 form = Form()
1159 else:
1160 form = super(VMWizardView, self).get_form(step, data, files)
1162 return form
1164 def get_context_data(self, form, **kwargs):
1165 context = super(VMWizardView, self).get_context_data(form=form,
1166 **kwargs)
1167 summary = {
1168 "cluster_form": self.get_cleaned_data_for_step("0"),
1169 "owner_form": self.get_cleaned_data_for_step("1"),
1170 "basics_form": self.get_cleaned_data_for_step("2"),
1171 "advanced_form": self.get_cleaned_data_for_step("3"),
1172 "hv_form": self.get_cleaned_data_for_step("4"),
1174 context["summary"] = summary
1176 return context
1178 def done(self, forms, template=None, **kwargs):
1180 Create a template. Optionally, bind a template to a VM instance
1181 created from the template. Optionally, name the template and save it.
1182 One or both of those is done depending on what the user has requested.
1185 # Hack: accepting kwargs in order to be able to work in several
1186 # different spots.
1188 if template is None:
1189 template = VirtualMachineTemplate()
1190 else:
1191 template = self._get_template()
1193 user = self.request.user
1195 cluster = forms[0].cleaned_data["cluster"]
1196 owner = forms[1].cleaned_data["owner"]
1198 template_name = forms[1].cleaned_data["template_name"]
1199 hostname = forms[1].cleaned_data["hostname"]
1201 # choice_data are the options that were not checked
1202 # if unchecked, than we should make sure that this is not submitted.
1203 # this fixes cases where the user checked a box in the beginning, put
1204 # data into the input, and went back and unchecked that box later.
1205 unchecked_options = self._get_vm_or_template()
1206 for unchecked in unchecked_options:
1207 if 'template_name' == unchecked:
1208 template_name = ''
1209 if 'hostname' == unchecked:
1210 hostname = ''
1212 template.cluster = cluster
1213 template.memory = forms[2].cleaned_data["memory"]
1214 if has_balloonmem(cluster):
1215 template.minmem = forms[2].cleaned_data["minram"]
1216 template.vcpus = forms[2].cleaned_data["vcpus"]
1217 template.disk_template = forms[2].cleaned_data["disk_template"]
1219 template.disks = forms[2].cleaned_data["disks"]
1221 nics = forms[2].cleaned_data["nics"]
1222 # default
1223 if not nics:
1224 nics = [{"link": "br0", "mode": "bridged"}]
1225 template.nics = nics
1227 template.os = forms[2].cleaned_data["os"]
1228 template.ip_check = forms[3].cleaned_data["ip_check"]
1229 template.name_check = forms[3].cleaned_data["name_check"]
1230 template.pnode = forms[3].cleaned_data["pnode"].hostname
1232 hvparams = forms[4].cleaned_data
1234 template.boot_order = hvparams.get("boot_order")
1235 template.cdrom2_image_path = hvparams.get("cdrom2_image_path")
1236 template.cdrom_image_path = hvparams.get("cdrom_image_path")
1237 template.kernel_path = hvparams.get("kernel_path")
1238 template.root_path = hvparams.get("root_path")
1239 template.serial_console = hvparams.get("serial_console")
1241 if "snode" in forms[3].cleaned_data:
1242 template.snode = forms[3].cleaned_data["snode"].hostname
1244 template.set_name(template_name)
1245 # only save the template to the database if its not temporary
1246 if not template.temporary:
1247 template.save()
1249 if hostname:
1250 vm = template_to_instance(template, hostname, owner)
1251 log_action('CREATE', user, vm)
1252 return HttpResponseRedirect(reverse('instance-detail',
1253 args=[cluster.slug,
1254 vm.hostname]))
1255 else:
1256 return HttpResponseRedirect(reverse("template-detail",
1257 args=[cluster.slug,
1258 template]))
1261 def vm_wizard(*args, **kwargs):
1262 forms = (
1263 VMWizardClusterForm,
1264 VMWizardOwnerForm,
1265 VMWizardBasicsForm,
1266 VMWizardAdvancedForm,
1267 Form,
1269 initial = kwargs.get('initial_dict', None)
1270 return VMWizardView.as_view(forms, initial_dict=initial)