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, initial
='',
772 required
=False, help_text
=_(VM_HELP
['nic_link']))
775 nic_mode
.widget
.attrs
['class'] = 'multi nic mode'
776 nic_mode
.widget
.attrs
['data-group'] = i
777 nic_link
.widget
.attrs
['class'] = 'multi nic link'
778 nic_link
.widget
.attrs
['data-group'] = i
780 self
.fields
['nic_mode_%s' % i
] = nic_mode
781 self
.fields
['nic_link_%s' % i
] = nic_link
783 def _configure_for_cluster(self
, cluster
):
787 self
.cluster
= cluster
789 # Get a look at the list of available hypervisors, and set the initial
790 # hypervisor appropriately.
791 hvs
= cluster
.info
["enabled_hypervisors"]
792 prettified
= [hv_prettify(hv
) for hv
in hvs
]
793 hv
= cluster
.info
["default_hypervisor"]
794 self
.fields
["hv"].choices
= zip(hvs
, prettified
)
795 self
.fields
["hv"].initial
= hv
797 if not has_sharedfile(cluster
):
798 self
.fields
["disk_template"].choices
.remove((u
'sharedfile',
802 self
.fields
["os"].choices
= cluster_os_list(cluster
)
804 # Set the default CPU count based on the backend parameters.
805 beparams
= cluster
.info
["beparams"]["default"]
806 self
.fields
["vcpus"].initial
= beparams
["vcpus"]
808 # Check for memory based on ganeti version
809 if has_balloonmem(cluster
):
810 self
.fields
["memory"].initial
= beparams
["maxmem"]
811 self
.fields
["minram"].initial
= beparams
["minmem"]
813 self
.fields
["memory"].initial
= beparams
["memory"]
815 # If there are ipolicy limits in place, add validators for them.
816 if "ipolicy" in cluster
.info
:
817 if "max" in cluster
.info
["ipolicy"]:
819 v
= cluster
.info
["ipolicy"]["max"]["disk-size"]
820 for disk
in xrange(settings
.MAX_DISKS_ADD
):
821 self
.fields
["disk_size_%s" % disk
].validators
.append(
822 MaxValueValidator(v
))
824 v
= cluster
.info
["ipolicy"]["max"]["memory-size"]
825 self
.fields
["memory"].validators
.append(MaxValueValidator(v
))
826 if has_balloonmem(cluster
):
827 self
.fields
["minram"].validators
.append(
828 MaxValueValidator(v
))
830 if "min" in cluster
.info
["ipolicy"]:
832 v
= cluster
.info
["ipolicy"]["min"]["disk-size"]
833 for disk
in xrange(settings
.MAX_DISKS_ADD
):
834 disk_field
= self
.fields
["disk_size_%s" % disk
]
835 disk_field
.validators
.append(MinValueValidator(v
))
836 # if its the first disk, add the min value as a default
838 disk_field
.initial
= v
840 v
= cluster
.info
["ipolicy"]["min"]["memory-size"]
841 self
.fields
["memory"].validators
.append(MinValueValidator(v
))
842 if has_balloonmem(cluster
):
843 self
.fields
["minram"].validators
.append(
844 MinValueValidator(v
))
846 # configure cluster defaults for nics
847 nic_defaults
= cluster
.info
['nicparams']['default']
848 self
.fields
['nic_mode_0'].initial
= nic_defaults
['mode']
849 self
.fields
['nic_link_0'].initial
= nic_defaults
['link']
851 def _configure_for_template(self
, template
):
855 self
.fields
["os"].initial
= template
.os
856 self
.fields
["vcpus"].initial
= template
.vcpus
857 self
.fields
["memory"].initial
= template
.memory
858 if has_balloonmem(template
.cluster
):
859 self
.fields
["minram"].initial
= template
.minmem
860 self
.fields
["disk_template"].initial
= template
.disk_template
861 for num
, disk
in enumerate(template
.disks
):
862 self
.fields
["disk_size_%s" % num
].initial
= disk
["size"]
863 for num
, nic
in enumerate(template
.nics
):
864 self
.fields
["nic_link_%s" % num
].initial
= nic
['link']
865 self
.fields
["nic_mode_%s" % num
].initial
= nic
['mode']
868 data
= self
.cleaned_data
869 # get disk sizes after validation (after 1.5G -> 1500)
870 # and filter empty fields.
872 for disk_num
in xrange(settings
.MAX_DISKS_ADD
):
873 disk
= data
.get("disk_size_%s" % disk_num
, None)
876 # if disks validated (no errors), but none of them contain data, then
877 # they were all left empty
878 if not disks
and not self
._errors
:
879 msg
= _("You need to add at least 1 disk!")
880 self
._errors
["disk_size_0"] = self
.error_class([msg
])
882 # Store disks as an array of dicts for use in template.
883 data
["disks"] = [{"size": disk
} for disk
in disks
]
886 for nic
in xrange(settings
.MAX_NICS_ADD
):
887 link
= data
.get('nic_link_%s' % nic
, None)
888 mode
= data
.get('nic_mode_%s' % nic
, None)
889 # if both the mode and link for a NIC are filled out, add it to the
892 nics
.append({'link': link
, 'mode': mode
})
894 raise ValidationError(_("Please input both a link and mode."))
898 if data
.get('minram') > data
.get('memory'):
899 msg
= _("The minimum ram cannot be larger than the maximum ram.")
900 self
._errors
["minram"] = self
.error_class([msg
])
905 class VMWizardAdvancedForm(Form
):
906 ip_check
= BooleanField(label
=_('Verify IP'), initial
=False,
908 help_text
=_(VM_RENAME_HELP
['ip_check']))
909 name_check
= BooleanField(label
=_('Verify hostname through DNS'),
910 initial
=False, required
=False,
911 help_text
=_(VM_RENAME_HELP
['name_check']))
912 pnode
= ModelChoiceField(label
=_("Primary Node"),
913 queryset
=Node
.objects
.all(), empty_label
=None,
914 help_text
=_(VM_CREATE_HELP
['pnode']))
915 snode
= ModelChoiceField(label
=_("Secondary Node"),
916 queryset
=Node
.objects
.all(), empty_label
=None,
917 help_text
=_(VM_CREATE_HELP
['snode']))
919 def _configure_for_cluster(self
, cluster
):
923 self
.cluster
= cluster
925 qs
= Node
.objects
.filter(cluster
=cluster
)
926 self
.fields
["pnode"].queryset
= qs
927 self
.fields
["snode"].queryset
= qs
929 def _configure_for_template(self
, template
):
933 self
.fields
["ip_check"].initial
= template
.ip_check
934 self
.fields
["name_check"].initial
= template
.name_check
935 self
.fields
["pnode"].initial
= template
.pnode
936 self
.fields
["snode"].initial
= template
.snode
938 def _configure_for_disk_template(self
, template
):
939 if template
!= "drbd":
940 del self
.fields
["snode"]
943 # Ganeti will error on VM creation if an IP address check is requested
944 # but a name check is not.
945 data
= self
.cleaned_data
946 if (data
.get("ip_check") and not data
.get("name_check")):
947 msg
= ["Cannot perform IP check without name check"]
948 self
._errors
["ip_check"] = self
.error_class(msg
)
950 if data
.get('pnode') == data
.get('snode'):
951 raise forms
.ValidationError("The secondary node cannot be the "
957 class VMWizardPVMForm(Form
):
958 kernel_path
= CharField(label
=_("Kernel path"), max_length
=255)
959 root_path
= CharField(label
=_("Root path"), max_length
=255)
961 def _configure_for_cluster(self
, cluster
):
965 self
.cluster
= cluster
966 params
= cluster
.info
["hvparams"]["xen-pvm"]
968 self
.fields
["kernel_path"].initial
= params
["kernel_path"]
969 self
.fields
["root_path"].initial
= params
["root_path"]
971 def _configure_for_template(self
, template
):
975 self
.fields
["kernel_path"].initial
= template
.kernel_path
976 self
.fields
["root_path"].initial
= template
.root_path
979 class VMWizardHVMForm(Form
):
980 boot_order
= ChoiceField(label
=_("Preferred boot device"),
981 required
=False, choices
=HVM_BOOT_ORDER
,
982 help_text
=_(VM_CREATE_HELP
['boot_order']))
983 cdrom_image_path
= CharField(label
=_("CD-ROM image path"), max_length
=512,
986 VM_CREATE_HELP
['cdrom_image_path']))
987 disk_type
= ChoiceField(label
=_("Disk type"),
988 choices
=HVM_CHOICES
["disk_type"],
989 help_text
=_(VM_CREATE_HELP
['disk_type']))
990 nic_type
= ChoiceField(label
=_("NIC type"),
991 choices
=HVM_CHOICES
["nic_type"],
992 help_text
=_(VM_CREATE_HELP
['nic_type']))
994 def _configure_for_cluster(self
, cluster
):
998 self
.cluster
= cluster
999 params
= cluster
.info
["hvparams"]["xen-pvm"]
1001 self
.fields
["boot_order"].initial
= params
["boot_order"]
1002 self
.fields
["disk_type"].initial
= params
["disk_type"]
1003 self
.fields
["nic_type"].initial
= params
["nic_type"]
1005 def _configure_for_template(self
, template
):
1009 self
.fields
["boot_order"].initial
= template
.boot_order
1010 self
.fields
["cdrom_image_path"].initial
= template
.cdrom_image_path
1011 self
.fields
["disk_type"].initial
= template
.disk_type
1012 self
.fields
["nic_type"].initial
= template
.nic_type
1015 class VMWizardKVMForm(Form
):
1016 kernel_path
= CharField(label
=_("Kernel path"), max_length
=255,
1018 help_text
=_(VM_CREATE_HELP
['kernel_path']))
1019 root_path
= CharField(label
=_("Root path"), max_length
=255,
1020 help_text
=_(VM_CREATE_HELP
['root_path']))
1021 serial_console
= BooleanField(label
=_("Enable serial console"),
1024 VM_CREATE_HELP
['serial_console']))
1025 boot_order
= ChoiceField(label
=_("Preferred boot device"),
1026 required
=False, choices
=KVM_BOOT_ORDER
,
1027 help_text
=_(VM_CREATE_HELP
['boot_order']))
1028 cdrom_image_path
= CharField(label
=_("CD-ROM image path"), max_length
=512,
1031 VM_CREATE_HELP
['cdrom_image_path']))
1032 cdrom2_image_path
= CharField(label
=_("Second CD-ROM image path"),
1033 max_length
=512, required
=False,
1035 VM_CREATE_HELP
['cdrom2_image_path']))
1036 disk_type
= ChoiceField(label
=_("Disk type"),
1037 choices
=KVM_CHOICES
["disk_type"],
1038 help_text
=_(VM_CREATE_HELP
['disk_type']))
1039 nic_type
= ChoiceField(label
=_("NIC type"),
1040 choices
=KVM_CHOICES
["nic_type"],
1041 help_text
=_(VM_CREATE_HELP
['nic_type']))
1043 def _configure_for_cluster(self
, cluster
):
1047 self
.cluster
= cluster
1048 params
= cluster
.info
["hvparams"]["kvm"]
1050 self
.fields
["boot_order"].initial
= params
["boot_order"]
1051 self
.fields
["disk_type"].initial
= params
["disk_type"]
1052 self
.fields
["kernel_path"].initial
= params
["kernel_path"]
1053 self
.fields
["nic_type"].initial
= params
["nic_type"]
1054 self
.fields
["root_path"].initial
= params
["root_path"]
1055 self
.fields
["serial_console"].initial
= params
["serial_console"]
1057 # Remove cdrom2 if the cluster doesn't have it; see #11655.
1058 if not has_cdrom2(cluster
):
1059 del self
.fields
["cdrom2_image_path"]
1061 def _configure_for_template(self
, template
):
1065 self
.fields
["kernel_path"].initial
= template
.kernel_path
1066 self
.fields
["root_path"].initial
= template
.root_path
1067 self
.fields
["serial_console"].initial
= template
.serial_console
1068 self
.fields
["boot_order"].initial
= template
.boot_order
1069 self
.fields
["cdrom_image_path"].initial
= template
.cdrom_image_path
1070 self
.fields
["cdrom2_image_path"].initial
= template
.cdrom2_image_path
1071 self
.fields
["disk_type"].initial
= template
.disk_type
1072 self
.fields
["nic_type"].initial
= template
.nic_type
1075 data
= super(VMWizardKVMForm
, self
).clean()
1077 # Force cdrom disk type to IDE; see #9297.
1078 data
['cdrom_disk_type'] = 'ide'
1080 # If booting from CD-ROM, require the first CD-ROM image to be
1082 if (data
.get("boot_order") == "cdrom" and
1083 not data
.get("cdrom_image_path")):
1084 msg
= u
"%s." % _("Image path required if boot device is CD-ROM")
1085 self
._errors
["cdrom_image_path"] = self
.error_class([msg
])
1090 class VMWizardView(LoginRequiredMixin
, CookieWizardView
):
1091 template_name
= "ganeti/forms/vm_wizard.html"
1094 # value, display value
1095 # value corresponds to VMWizardOwnerForm's fields
1096 ('template_name', 'Template'),
1097 ('hostname', 'Virtual Machine'),
1100 def _get_vm_or_template(self
):
1101 """Returns items that were not checked in step0"""
1102 data
= self
.get_cleaned_data_for_step('0')
1104 options
= [option
[0] for option
in self
.OPTIONS
]
1105 choices
= data
.get('choices', None)
1106 # which boxes weren't checked
1107 unchecked
= set(options
) - set(choices
)
1112 def _get_template(self
):
1113 name
= self
.kwargs
.get("template")
1115 return VirtualMachineTemplate
.objects
.get(template_name
=name
)
1118 def _get_cluster(self
):
1119 data
= self
.get_cleaned_data_for_step("0")
1121 return data
["cluster"]
1125 data
= self
.get_cleaned_data_for_step("2")
1130 def _get_disk_template(self
):
1131 data
= self
.get_cleaned_data_for_step("2")
1133 return data
["disk_template"]
1136 def get_form(self
, step
=None, data
=None, files
=None):
1137 s
= int(self
.steps
.current
) if step
is None else int(step
)
1138 initial
= self
.get_form_initial(s
)
1141 form
= VMWizardClusterForm(data
=data
, options
=self
.OPTIONS
,
1143 form
._configure
_for
_user
(self
.request
.user
)
1144 # XXX this should somehow become totally invalid if the user
1145 # doesn't have perms on the template.
1147 form
= VMWizardOwnerForm(data
=data
)
1148 form
._configure
_for
_cluster
(self
._get
_cluster
())
1149 form
._configure
_for
_template
(self
._get
_template
(),
1150 choices
=self
._get
_vm
_or
_template
())
1152 form
= VMWizardBasicsForm(data
=data
)
1153 form
._configure
_for
_cluster
(self
._get
_cluster
())
1154 form
._configure
_for
_template
(self
._get
_template
())
1156 form
= VMWizardAdvancedForm(data
=data
)
1157 form
._configure
_for
_cluster
(self
._get
_cluster
())
1158 form
._configure
_for
_template
(self
._get
_template
())
1159 form
._configure
_for
_disk
_template
(self
._get
_disk
_template
())
1161 cluster
= self
._get
_cluster
()
1167 form
= VMWizardKVMForm(data
=data
)
1168 elif hv
== "xen-pvm":
1169 form
= VMWizardPVMForm(data
=data
)
1170 elif hv
== "xen-hvm":
1171 form
= VMWizardHVMForm(data
=data
)
1174 form
._configure
_for
_cluster
(cluster
)
1175 form
._configure
_for
_template
(self
._get
_template
())
1179 form
= super(VMWizardView
, self
).get_form(step
, data
, files
)
1183 def get_context_data(self
, form
, **kwargs
):
1184 context
= super(VMWizardView
, self
).get_context_data(form
=form
,
1187 "cluster_form": self
.get_cleaned_data_for_step("0"),
1188 "owner_form": self
.get_cleaned_data_for_step("1"),
1189 "basics_form": self
.get_cleaned_data_for_step("2"),
1190 "advanced_form": self
.get_cleaned_data_for_step("3"),
1191 "hv_form": self
.get_cleaned_data_for_step("4"),
1193 context
["summary"] = summary
1197 def done(self
, forms
, template
=None, **kwargs
):
1199 Create a template. Optionally, bind a template to a VM instance
1200 created from the template. Optionally, name the template and save it.
1201 One or both of those is done depending on what the user has requested.
1204 # Hack: accepting kwargs in order to be able to work in several
1207 if template
is None:
1208 template
= VirtualMachineTemplate()
1210 template
= self
._get
_template
()
1212 user
= self
.request
.user
1214 cluster
= forms
[0].cleaned_data
["cluster"]
1215 owner
= forms
[1].cleaned_data
["owner"]
1217 template_name
= forms
[1].cleaned_data
["template_name"]
1218 hostname
= forms
[1].cleaned_data
["hostname"]
1220 # choice_data are the options that were not checked
1221 # if unchecked, than we should make sure that this is not submitted.
1222 # this fixes cases where the user checked a box in the beginning, put
1223 # data into the input, and went back and unchecked that box later.
1224 unchecked_options
= self
._get
_vm
_or
_template
()
1225 for unchecked
in unchecked_options
:
1226 if 'template_name' == unchecked
:
1228 if 'hostname' == unchecked
:
1231 template
.cluster
= cluster
1232 template
.memory
= forms
[2].cleaned_data
["memory"]
1233 if has_balloonmem(cluster
):
1234 template
.minmem
= forms
[2].cleaned_data
["minram"]
1235 template
.vcpus
= forms
[2].cleaned_data
["vcpus"]
1236 template
.disk_template
= forms
[2].cleaned_data
["disk_template"]
1238 template
.disks
= forms
[2].cleaned_data
["disks"]
1240 nics
= forms
[2].cleaned_data
["nics"]
1243 nics
= [{"link": "br0", "mode": "bridged"}]
1244 template
.nics
= nics
1246 template
.os
= forms
[2].cleaned_data
["os"]
1247 template
.ip_check
= forms
[3].cleaned_data
["ip_check"]
1248 template
.name_check
= forms
[3].cleaned_data
["name_check"]
1249 template
.pnode
= forms
[3].cleaned_data
["pnode"].hostname
1251 hvparams
= forms
[4].cleaned_data
1253 template
.boot_order
= hvparams
.get("boot_order")
1254 template
.cdrom2_image_path
= hvparams
.get("cdrom2_image_path")
1255 template
.cdrom_image_path
= hvparams
.get("cdrom_image_path")
1256 template
.kernel_path
= hvparams
.get("kernel_path")
1257 template
.root_path
= hvparams
.get("root_path")
1258 template
.serial_console
= hvparams
.get("serial_console")
1260 if "snode" in forms
[3].cleaned_data
:
1261 template
.snode
= forms
[3].cleaned_data
["snode"].hostname
1263 template
.set_name(template_name
)
1264 # only save the template to the database if its not temporary
1265 if not template
.temporary
:
1269 vm
= template_to_instance(template
, hostname
, owner
)
1270 log_action('CREATE', user
, vm
)
1271 return HttpResponseRedirect(reverse('instance-detail',
1275 return HttpResponseRedirect(reverse("template-detail",
1280 def vm_wizard(*args
, **kwargs
):
1282 VMWizardClusterForm
,
1285 VMWizardAdvancedForm
,
1288 initial
= kwargs
.get('initial_dict', None)
1289 return VMWizardView
.as_view(forms
, initial_dict
=initial
)