VMWizardAdvanced now prevents snode == pnode
[ganeti_webmgr.git] / ganeti_web / forms / virtual_machine.py
blob28c0049a27d0ff78bb5f483bd6c933afd9c474e1
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["memory"]
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(template.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 data = self.cleaned_data
932 if (data.get("ip_check") and not data.get("name_check")):
933 msg = ["Cannot perform IP check without name check"]
934 self._errors["ip_check"] = self.error_class(msg)
936 if data.get('pnode') == data.get('snode'):
937 raise forms.ValidationError("The secondary node cannot be the "
938 "primary node.")
940 return data
943 class VMWizardPVMForm(Form):
944 kernel_path = CharField(label=_("Kernel path"), max_length=255)
945 root_path = CharField(label=_("Root path"), max_length=255)
947 def _configure_for_cluster(self, cluster):
948 if not cluster:
949 return
951 self.cluster = cluster
952 params = cluster.info["hvparams"]["xen-pvm"]
954 self.fields["kernel_path"].initial = params["kernel_path"]
955 self.fields["root_path"].initial = params["root_path"]
957 def _configure_for_template(self, template):
958 if not template:
959 return
961 self.fields["kernel_path"].initial = template.kernel_path
962 self.fields["root_path"].initial = template.root_path
965 class VMWizardHVMForm(Form):
966 boot_order = ChoiceField(label=_("Preferred boot device"),
967 required=False, choices=HVM_BOOT_ORDER,
968 help_text=_(VM_CREATE_HELP['boot_order']))
969 cdrom_image_path = CharField(label=_("CD-ROM image path"), max_length=512,
970 required=False,
971 help_text=_(
972 VM_CREATE_HELP['cdrom_image_path']))
973 disk_type = ChoiceField(label=_("Disk type"),
974 choices=HVM_CHOICES["disk_type"],
975 help_text=_(VM_CREATE_HELP['disk_type']))
976 nic_type = ChoiceField(label=_("NIC type"),
977 choices=HVM_CHOICES["nic_type"],
978 help_text=_(VM_CREATE_HELP['nic_type']))
980 def _configure_for_cluster(self, cluster):
981 if not cluster:
982 return
984 self.cluster = cluster
985 params = cluster.info["hvparams"]["xen-pvm"]
987 self.fields["boot_order"].initial = params["boot_order"]
988 self.fields["disk_type"].initial = params["disk_type"]
989 self.fields["nic_type"].initial = params["nic_type"]
991 def _configure_for_template(self, template):
992 if not template:
993 return
995 self.fields["boot_order"].initial = template.boot_order
996 self.fields["cdrom_image_path"].initial = template.cdrom_image_path
997 self.fields["disk_type"].initial = template.disk_type
998 self.fields["nic_type"].initial = template.nic_type
1001 class VMWizardKVMForm(Form):
1002 kernel_path = CharField(label=_("Kernel path"), max_length=255,
1003 help_text=_(VM_CREATE_HELP['kernel_path']))
1004 root_path = CharField(label=_("Root path"), max_length=255,
1005 help_text=_(VM_CREATE_HELP['root_path']))
1006 serial_console = BooleanField(label=_("Enable serial console"),
1007 required=False,
1008 help_text=_(
1009 VM_CREATE_HELP['serial_console']))
1010 boot_order = ChoiceField(label=_("Preferred boot device"),
1011 required=False, choices=KVM_BOOT_ORDER,
1012 help_text=_(VM_CREATE_HELP['boot_order']))
1013 cdrom_image_path = CharField(label=_("CD-ROM image path"), max_length=512,
1014 required=False,
1015 help_text=_(
1016 VM_CREATE_HELP['cdrom_image_path']))
1017 cdrom2_image_path = CharField(label=_("Second CD-ROM image path"),
1018 max_length=512, required=False,
1019 help_text=_(
1020 VM_CREATE_HELP['cdrom2_image_path']))
1021 disk_type = ChoiceField(label=_("Disk type"),
1022 choices=KVM_CHOICES["disk_type"],
1023 help_text=_(VM_CREATE_HELP['disk_type']))
1024 nic_type = ChoiceField(label=_("NIC type"),
1025 choices=KVM_CHOICES["nic_type"],
1026 help_text=_(VM_CREATE_HELP['nic_type']))
1028 def _configure_for_cluster(self, cluster):
1029 if not cluster:
1030 return
1032 self.cluster = cluster
1033 params = cluster.info["hvparams"]["kvm"]
1035 self.fields["boot_order"].initial = params["boot_order"]
1036 self.fields["disk_type"].initial = params["disk_type"]
1037 self.fields["kernel_path"].initial = params["kernel_path"]
1038 self.fields["nic_type"].initial = params["nic_type"]
1039 self.fields["root_path"].initial = params["root_path"]
1040 self.fields["serial_console"].initial = params["serial_console"]
1042 # Remove cdrom2 if the cluster doesn't have it; see #11655.
1043 if not has_cdrom2(cluster):
1044 del self.fields["cdrom2_image_path"]
1046 def _configure_for_template(self, template):
1047 if not template:
1048 return
1050 self.fields["kernel_path"].initial = template.kernel_path
1051 self.fields["root_path"].initial = template.root_path
1052 self.fields["serial_console"].initial = template.serial_console
1053 self.fields["boot_order"].initial = template.boot_order
1054 self.fields["cdrom_image_path"].initial = template.cdrom_image_path
1055 self.fields["cdrom2_image_path"].initial = template.cdrom2_image_path
1056 self.fields["disk_type"].initial = template.disk_type
1057 self.fields["nic_type"].initial = template.nic_type
1059 def clean(self):
1060 data = super(VMWizardKVMForm, self).clean()
1062 # Force cdrom disk type to IDE; see #9297.
1063 data['cdrom_disk_type'] = 'ide'
1065 # If booting from CD-ROM, require the first CD-ROM image to be
1066 # present.
1067 if (data.get("boot_order") == "cdrom" and
1068 not data.get("cdrom_image_path")):
1069 msg = u"%s." % _("Image path required if boot device is CD-ROM")
1070 self._errors["cdrom_image_path"] = self.error_class([msg])
1072 return data
1075 class VMWizardView(LoginRequiredMixin, CookieWizardView):
1076 template_name = "ganeti/forms/vm_wizard.html"
1078 OPTIONS = (
1079 # value, display value
1080 # value corresponds to VMWizardOwnerForm's fields
1081 ('template_name', 'Template'),
1082 ('hostname', 'Virtual Machine'),
1085 def _get_vm_or_template(self):
1086 """Returns items that were not checked in step0"""
1087 data = self.get_cleaned_data_for_step('0')
1088 if data:
1089 options = [option[0] for option in self.OPTIONS]
1090 choices = data.get('choices', None)
1091 # which boxes weren't checked
1092 unchecked = set(options) - set(choices)
1093 return unchecked
1095 return None
1097 def _get_template(self):
1098 name = self.kwargs.get("template")
1099 if name:
1100 return VirtualMachineTemplate.objects.get(template_name=name)
1101 return None
1103 def _get_cluster(self):
1104 data = self.get_cleaned_data_for_step("0")
1105 if data:
1106 return data["cluster"]
1107 return None
1109 def _get_hv(self):
1110 data = self.get_cleaned_data_for_step("2")
1111 if data:
1112 return data["hv"]
1113 return None
1115 def _get_disk_template(self):
1116 data = self.get_cleaned_data_for_step("2")
1117 if data:
1118 return data["disk_template"]
1119 return None
1121 def get_form(self, step=None, data=None, files=None):
1122 s = int(self.steps.current) if step is None else int(step)
1123 initial = self.get_form_initial(s)
1125 if s == 0:
1126 form = VMWizardClusterForm(data=data, options=self.OPTIONS,
1127 initial=initial)
1128 form._configure_for_user(self.request.user)
1129 # XXX this should somehow become totally invalid if the user
1130 # doesn't have perms on the template.
1131 elif s == 1:
1132 form = VMWizardOwnerForm(data=data)
1133 form._configure_for_cluster(self._get_cluster())
1134 form._configure_for_template(self._get_template(),
1135 choices=self._get_vm_or_template())
1136 elif s == 2:
1137 form = VMWizardBasicsForm(data=data)
1138 form._configure_for_cluster(self._get_cluster())
1139 form._configure_for_template(self._get_template())
1140 elif s == 3:
1141 form = VMWizardAdvancedForm(data=data)
1142 form._configure_for_cluster(self._get_cluster())
1143 form._configure_for_template(self._get_template())
1144 form._configure_for_disk_template(self._get_disk_template())
1145 elif s == 4:
1146 cluster = self._get_cluster()
1147 hv = self._get_hv()
1148 form = None
1150 if cluster and hv:
1151 if hv == "kvm":
1152 form = VMWizardKVMForm(data=data)
1153 elif hv == "xen-pvm":
1154 form = VMWizardPVMForm(data=data)
1155 elif hv == "xen-hvm":
1156 form = VMWizardHVMForm(data=data)
1158 if form:
1159 form._configure_for_cluster(cluster)
1160 form._configure_for_template(self._get_template())
1161 else:
1162 form = Form()
1163 else:
1164 form = super(VMWizardView, self).get_form(step, data, files)
1166 return form
1168 def get_context_data(self, form, **kwargs):
1169 context = super(VMWizardView, self).get_context_data(form=form,
1170 **kwargs)
1171 summary = {
1172 "cluster_form": self.get_cleaned_data_for_step("0"),
1173 "owner_form": self.get_cleaned_data_for_step("1"),
1174 "basics_form": self.get_cleaned_data_for_step("2"),
1175 "advanced_form": self.get_cleaned_data_for_step("3"),
1176 "hv_form": self.get_cleaned_data_for_step("4"),
1178 context["summary"] = summary
1180 return context
1182 def done(self, forms, template=None, **kwargs):
1184 Create a template. Optionally, bind a template to a VM instance
1185 created from the template. Optionally, name the template and save it.
1186 One or both of those is done depending on what the user has requested.
1189 # Hack: accepting kwargs in order to be able to work in several
1190 # different spots.
1192 if template is None:
1193 template = VirtualMachineTemplate()
1194 else:
1195 template = self._get_template()
1197 user = self.request.user
1199 cluster = forms[0].cleaned_data["cluster"]
1200 owner = forms[1].cleaned_data["owner"]
1202 template_name = forms[1].cleaned_data["template_name"]
1203 hostname = forms[1].cleaned_data["hostname"]
1205 # choice_data are the options that were not checked
1206 # if unchecked, than we should make sure that this is not submitted.
1207 # this fixes cases where the user checked a box in the beginning, put
1208 # data into the input, and went back and unchecked that box later.
1209 unchecked_options = self._get_vm_or_template()
1210 for unchecked in unchecked_options:
1211 if 'template_name' == unchecked:
1212 template_name = ''
1213 if 'hostname' == unchecked:
1214 hostname = ''
1216 template.cluster = cluster
1217 template.memory = forms[2].cleaned_data["memory"]
1218 if has_balloonmem(cluster):
1219 template.minmem = forms[2].cleaned_data["minram"]
1220 template.vcpus = forms[2].cleaned_data["vcpus"]
1221 template.disk_template = forms[2].cleaned_data["disk_template"]
1223 template.disks = forms[2].cleaned_data["disks"]
1225 nics = forms[2].cleaned_data["nics"]
1226 # default
1227 if not nics:
1228 nics = [{"link": "br0", "mode": "bridged"}]
1229 template.nics = nics
1231 template.os = forms[2].cleaned_data["os"]
1232 template.ip_check = forms[3].cleaned_data["ip_check"]
1233 template.name_check = forms[3].cleaned_data["name_check"]
1234 template.pnode = forms[3].cleaned_data["pnode"].hostname
1236 hvparams = forms[4].cleaned_data
1238 template.boot_order = hvparams.get("boot_order")
1239 template.cdrom2_image_path = hvparams.get("cdrom2_image_path")
1240 template.cdrom_image_path = hvparams.get("cdrom_image_path")
1241 template.kernel_path = hvparams.get("kernel_path")
1242 template.root_path = hvparams.get("root_path")
1243 template.serial_console = hvparams.get("serial_console")
1245 if "snode" in forms[3].cleaned_data:
1246 template.snode = forms[3].cleaned_data["snode"].hostname
1248 template.set_name(template_name)
1249 # only save the template to the database if its not temporary
1250 if not template.temporary:
1251 template.save()
1253 if hostname:
1254 vm = template_to_instance(template, hostname, owner)
1255 log_action('CREATE', user, vm)
1256 return HttpResponseRedirect(reverse('instance-detail',
1257 args=[cluster.slug,
1258 vm.hostname]))
1259 else:
1260 return HttpResponseRedirect(reverse("template-detail",
1261 args=[cluster.slug,
1262 template]))
1265 def vm_wizard(*args, **kwargs):
1266 forms = (
1267 VMWizardClusterForm,
1268 VMWizardOwnerForm,
1269 VMWizardBasicsForm,
1270 VMWizardAdvancedForm,
1271 Form,
1273 initial = kwargs.get('initial_dict', None)
1274 return VMWizardView.as_view(forms, initial_dict=initial)