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,
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
,
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
):
59 Parent class that holds all vm clean methods
60 and shared form fields.
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)
69 model
= VirtualMachineTemplate
71 def create_disk_fields(self
, count
):
73 dynamically add fields for disks
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):
83 dynamically add fields for nics
85 self
.nic_fields
= range(count
)
86 for i
in range(count
):
87 nic_mode
= forms
.ChoiceField(label
=_('NIC/%s Mode' % i
),
89 nic_link
= forms
.CharField(label
=_('NIC/%s Link' % i
),
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
106 vm
= VirtualMachine
.objects
.get(cluster
=cluster
,
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
115 msg
= _("Owner cannot be changed when recovering a "
117 self
._errors
["owner"] = self
.error_class([msg
])
119 raise ValidationError(_("Hostname is already in use for "
122 except VirtualMachine
.DoesNotExist
:
123 # doesn't exist, no further checks needed
126 # Spaces in hostname will always break things.
128 self
._errors
["hostname"] = self
.error_class(
129 ["Hostname contains illegal character"])
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"])
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
])
148 def clean_security_domain(self
):
149 data
= self
.cleaned_data
['security_domain']
150 security_model
= self
.cleaned_data
['security_model']
153 if data
and security_model
!= 'user':
155 'This field can not be set if Security '
156 'Mode is not set to User')
157 elif security_model
== 'user':
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')
165 self
._errors
['security_domain'] = self
.error_class([msg
])
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
])
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
184 if owner
is not None:
185 start
= data
['start']
186 quota
= cluster
.get_quota(owner
)
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']):
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'] > \
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']
209 quota
['virtual_cpus']):
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 "
215 form
._errors
["vcpus"] = form
.error_class([q_msg
])
218 class ModifyVirtualMachineForm(VirtualMachineForm
):
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
])
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')
248 except AttributeError:
249 self
.owner
= vm
.owner
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
261 for field
in self
.required
:
262 self
.fields
[field
].required
= True
263 except AttributeError:
266 # Need to set initial values from vm.info as these are not saved
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
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']
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
298 mac
.initial
= info
['nic.macs'][i
]
299 link
.initial
= info
['nic.links'][i
]
301 self
.fields
['os'].initial
= info
['os']
304 if self
.hvparam_fields
:
305 for field
in self
.hvparam_fields
:
306 self
.fields
[field
].initial
= hvparam
.get(field
)
307 except AttributeError:
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 "
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 "
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
])
336 data
['start'] = 'reboot' in self
.data
or self
.vm
.is_running
337 check_quota_modify(self
)
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
346 self
._errors
[link_field
] = self
.error_class([_('This field is'
348 elif link
and not mac
:
349 self
._errors
[mac_field
] = self
.error_class([_('This field is '
351 data
['nic_count_original'] = self
.nics
356 class HvmModifyVirtualMachineForm(ModifyVirtualMachineForm
):
357 hvparam_fields
= ('boot_order', 'cdrom_image_path', 'nic_type',
358 'disk_type', 'vnc_bind_address', 'acpi',
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',
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',
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',
397 def __init__(self
, vm
, *args
, **kwargs
):
398 super(PvmModifyVirtualMachineForm
, self
).__init
__(vm
, *args
, **kwargs
)
401 class KvmModifyVirtualMachineForm(PvmModifyVirtualMachineForm
,
402 HvmModifyVirtualMachineForm
):
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',
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,
425 kvm_flag
= forms
.ChoiceField(label
='KVM Flag', required
=False,
427 mem_path
= forms
.CharField(label
='Mem Path', required
=False)
428 migration_downtime
= forms
.IntegerField(label
='Migration Downtime',
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,
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',
441 class Meta(ModifyVirtualMachineForm
.Meta
):
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
):
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
462 if data
.get('maxmem'):
463 cleaned
['maxmem'] = data
['maxmem']
464 cleaned
['minmem'] = data
['minmem']
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
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.
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', {}))
489 del data
['nic_mac_%s' % (nic_count
+i
)]
492 del data
['nic_link_%s' % (nic_count
+i
)]
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 "
506 class RenameForm(forms
.Form
):
507 """ form used for renaming a Virtual Machine """
508 hostname
= forms
.CharField(label
=_('Instance Name'), max_length
=255,
510 ip_check
= forms
.BooleanField(initial
=True, required
=False,
512 name_check
= forms
.BooleanField(initial
=True, required
=False,
513 label
=_('DNS Name Check'))
515 def __init__(self
, vm
, *args
, **kwargs
):
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"))
528 class ChangeOwnerForm(forms
.Form
):
529 """ Form used when modifying the owner of a virtual machine """
530 owner
= forms
.ModelChoiceField(queryset
=ClusterUser
.objects
.all(),
534 class ReplaceDisksForm(forms
.Form
):
536 Form used when replacing disks for a virtual machine
538 empty_field
= EMPTY_CHOICE_FIELD
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
],
551 iallocator
= forms
.BooleanField(initial
=False, label
=_('Iallocator'),
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'],
578 widget
=forms
.HiddenInput())
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
])
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
)
604 def clean_node(self
):
605 node
= self
.cleaned_data
.get('node')
606 return node
if node
else None
610 Start a replace disks job using the data in this form.
612 data
= self
.cleaned_data
614 disks
= data
['disks']
616 if data
['iallocator']:
617 iallocator
= data
['iallocator_hostname']
620 return self
.instance
.replace_disks(mode
, disks
, node
, iallocator
)
623 class VMWizardClusterForm(Form
):
624 cluster
= ModelChoiceField(label
=_('Cluster'),
625 queryset
=Cluster
.objects
.all(),
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
)
637 self
.fields
['choices'] = MultipleChoiceField(
638 widget
=CheckboxSelectMultiple
,
640 initial
=self
.initial
,
641 label
=_('What would you '
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
])
662 class VMWizardOwnerForm(Form
):
663 owner
= ModelChoiceField(label
=_('Owner'),
664 queryset
=ClusterUser
.objects
.all(),
666 help_text
=_(VM_CREATE_HELP
['owner']))
667 template_name
= CharField(label
=_("Template Name"), max_length
=255,
669 help_text
=_(VM_HELP
['template_name']))
670 hostname
= CharField(label
=_('Instance Name'), max_length
=255,
672 help_text
=_(VM_CREATE_HELP
['hostname']))
674 def _configure_for_cluster(self
, cluster
):
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
686 for field
in choices
:
688 self
.fields
[field
].widget
= forms
.HiddenInput()
693 self
.fields
["template_name"].initial
= template
.template_name
695 def clean_hostname(self
):
696 hostname
= self
.cleaned_data
.get('hostname')
698 # Confirm that the hostname is not already in use.
700 vm
= VirtualMachine
.objects
.get(cluster
=self
.cluster
,
702 except VirtualMachine
.DoesNotExist
:
703 # Well, *I'm* convinced.
706 raise ValidationError(
707 _("Hostname is already in use for this cluster"))
709 # Spaces in hostname will always break things.
711 self
._errors
["hostname"] = self
.error_class(
712 ["Hostnames cannot contain spaces."])
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
):
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',
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"]
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
):
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']
854 data
= self
.cleaned_data
855 # get disk sizes after validation (after 1.5G -> 1500)
856 # and filter empty fields.
858 for disk_num
in xrange(settings
.MAX_DISKS_ADD
):
859 disk
= data
.get("disk_size_%s" % disk_num
, None)
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
]
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
878 nics
.append({'link': link
, 'mode': mode
})
880 raise ValidationError(_("Please input both a link and mode."))
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
])
891 class VMWizardAdvancedForm(Form
):
892 ip_check
= BooleanField(label
=_('Verify IP'), initial
=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
):
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
):
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"]
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 "
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
):
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
):
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,
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
):
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
):
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"),
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,
1016 VM_CREATE_HELP
['cdrom_image_path']))
1017 cdrom2_image_path
= CharField(label
=_("Second CD-ROM image path"),
1018 max_length
=512, required
=False,
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
):
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
):
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
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
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
])
1075 class VMWizardView(LoginRequiredMixin
, CookieWizardView
):
1076 template_name
= "ganeti/forms/vm_wizard.html"
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')
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
)
1097 def _get_template(self
):
1098 name
= self
.kwargs
.get("template")
1100 return VirtualMachineTemplate
.objects
.get(template_name
=name
)
1103 def _get_cluster(self
):
1104 data
= self
.get_cleaned_data_for_step("0")
1106 return data
["cluster"]
1110 data
= self
.get_cleaned_data_for_step("2")
1115 def _get_disk_template(self
):
1116 data
= self
.get_cleaned_data_for_step("2")
1118 return data
["disk_template"]
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
)
1126 form
= VMWizardClusterForm(data
=data
, options
=self
.OPTIONS
,
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.
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
())
1137 form
= VMWizardBasicsForm(data
=data
)
1138 form
._configure
_for
_cluster
(self
._get
_cluster
())
1139 form
._configure
_for
_template
(self
._get
_template
())
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
())
1146 cluster
= self
._get
_cluster
()
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
)
1159 form
._configure
_for
_cluster
(cluster
)
1160 form
._configure
_for
_template
(self
._get
_template
())
1164 form
= super(VMWizardView
, self
).get_form(step
, data
, files
)
1168 def get_context_data(self
, form
, **kwargs
):
1169 context
= super(VMWizardView
, self
).get_context_data(form
=form
,
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
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
1192 if template
is None:
1193 template
= VirtualMachineTemplate()
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
:
1213 if 'hostname' == unchecked
:
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"]
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
:
1254 vm
= template_to_instance(template
, hostname
, owner
)
1255 log_action('CREATE', user
, vm
)
1256 return HttpResponseRedirect(reverse('instance-detail',
1260 return HttpResponseRedirect(reverse("template-detail",
1265 def vm_wizard(*args
, **kwargs
):
1267 VMWizardClusterForm
,
1270 VMWizardAdvancedForm
,
1273 initial
= kwargs
.get('initial_dict', None)
1274 return VMWizardView
.as_view(forms
, initial_dict
=initial
)