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
["memeory"]
814 # If there are ipolicy limits in place, add validators for them.
815 if "ipolicy" in cluster
.info
:
816 if "max" in cluster
.info
["ipolicy"]:
817 v
= cluster
.info
["ipolicy"]["max"]["disk-size"]
818 for disk
in xrange(settings
.MAX_DISKS_ADD
):
819 self
.fields
["disk_size_%s" % disk
].validators
.append(
820 MaxValueValidator(v
))
821 v
= cluster
.info
["ipolicy"]["max"]["memory-size"]
822 self
.fields
["memory"].validators
.append(MaxValueValidator(v
))
823 if has_balloonmem(cluster
):
824 self
.fields
["minram"].validators
.append(
825 MaxValueValidator(v
))
826 if "min" in cluster
.info
["ipolicy"]:
827 v
= cluster
.info
["ipolicy"]["min"]["disk-size"]
828 for disk
in xrange(settings
.MAX_DISKS_ADD
):
829 self
.fields
["disk_size_%s" % disk
].validators
.append(
830 MinValueValidator(v
))
831 v
= cluster
.info
["ipolicy"]["min"]["memory-size"]
832 self
.fields
["memory"].validators
.append(MinValueValidator(v
))
833 if has_balloonmem(cluster
):
834 self
.fields
["minram"].validators
.append(
835 MinValueValidator(v
))
837 def _configure_for_template(self
, template
):
841 self
.fields
["os"].initial
= template
.os
842 self
.fields
["vcpus"].initial
= template
.vcpus
843 self
.fields
["memory"].initial
= template
.memory
844 if has_balloonmem(cluster
):
845 self
.fields
["minram"].initial
= template
.minmem
846 self
.fields
["disk_template"].initial
= template
.disk_template
847 for num
, disk
in enumerate(template
.disks
):
848 self
.fields
["disk_size_%s" % num
].initial
= disk
["size"]
849 for num
, nic
in enumerate(template
.nics
):
850 self
.fields
["nic_link_%s" % num
].initial
= nic
['link']
851 self
.fields
["nic_mode_%s" % num
].initial
= nic
['mode']
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 if (self
.cleaned_data
.get("ip_check") and not
932 self
.cleaned_data
.get("name_check")):
933 msg
= ["Cannot perform IP check without name check"]
934 self
.errors
["ip_check"] = self
.error_class(msg
)
936 return self
.cleaned_data
939 class VMWizardPVMForm(Form
):
940 kernel_path
= CharField(label
=_("Kernel path"), max_length
=255)
941 root_path
= CharField(label
=_("Root path"), max_length
=255)
943 def _configure_for_cluster(self
, cluster
):
947 self
.cluster
= cluster
948 params
= cluster
.info
["hvparams"]["xen-pvm"]
950 self
.fields
["kernel_path"].initial
= params
["kernel_path"]
951 self
.fields
["root_path"].initial
= params
["root_path"]
953 def _configure_for_template(self
, template
):
957 self
.fields
["kernel_path"].initial
= template
.kernel_path
958 self
.fields
["root_path"].initial
= template
.root_path
961 class VMWizardHVMForm(Form
):
962 boot_order
= ChoiceField(label
=_("Preferred boot device"),
963 required
=False, choices
=HVM_BOOT_ORDER
,
964 help_text
=_(VM_CREATE_HELP
['boot_order']))
965 cdrom_image_path
= CharField(label
=_("CD-ROM image path"), max_length
=512,
968 VM_CREATE_HELP
['cdrom_image_path']))
969 disk_type
= ChoiceField(label
=_("Disk type"),
970 choices
=HVM_CHOICES
["disk_type"],
971 help_text
=_(VM_CREATE_HELP
['disk_type']))
972 nic_type
= ChoiceField(label
=_("NIC type"),
973 choices
=HVM_CHOICES
["nic_type"],
974 help_text
=_(VM_CREATE_HELP
['nic_type']))
976 def _configure_for_cluster(self
, cluster
):
980 self
.cluster
= cluster
981 params
= cluster
.info
["hvparams"]["xen-pvm"]
983 self
.fields
["boot_order"].initial
= params
["boot_order"]
984 self
.fields
["disk_type"].initial
= params
["disk_type"]
985 self
.fields
["nic_type"].initial
= params
["nic_type"]
987 def _configure_for_template(self
, template
):
991 self
.fields
["boot_order"].initial
= template
.boot_order
992 self
.fields
["cdrom_image_path"].initial
= template
.cdrom_image_path
993 self
.fields
["disk_type"].initial
= template
.disk_type
994 self
.fields
["nic_type"].initial
= template
.nic_type
997 class VMWizardKVMForm(Form
):
998 kernel_path
= CharField(label
=_("Kernel path"), max_length
=255,
999 help_text
=_(VM_CREATE_HELP
['kernel_path']))
1000 root_path
= CharField(label
=_("Root path"), max_length
=255,
1001 help_text
=_(VM_CREATE_HELP
['root_path']))
1002 serial_console
= BooleanField(label
=_("Enable serial console"),
1005 VM_CREATE_HELP
['serial_console']))
1006 boot_order
= ChoiceField(label
=_("Preferred boot device"),
1007 required
=False, choices
=KVM_BOOT_ORDER
,
1008 help_text
=_(VM_CREATE_HELP
['boot_order']))
1009 cdrom_image_path
= CharField(label
=_("CD-ROM image path"), max_length
=512,
1012 VM_CREATE_HELP
['cdrom_image_path']))
1013 cdrom2_image_path
= CharField(label
=_("Second CD-ROM image path"),
1014 max_length
=512, required
=False,
1016 VM_CREATE_HELP
['cdrom2_image_path']))
1017 disk_type
= ChoiceField(label
=_("Disk type"),
1018 choices
=KVM_CHOICES
["disk_type"],
1019 help_text
=_(VM_CREATE_HELP
['disk_type']))
1020 nic_type
= ChoiceField(label
=_("NIC type"),
1021 choices
=KVM_CHOICES
["nic_type"],
1022 help_text
=_(VM_CREATE_HELP
['nic_type']))
1024 def _configure_for_cluster(self
, cluster
):
1028 self
.cluster
= cluster
1029 params
= cluster
.info
["hvparams"]["kvm"]
1031 self
.fields
["boot_order"].initial
= params
["boot_order"]
1032 self
.fields
["disk_type"].initial
= params
["disk_type"]
1033 self
.fields
["kernel_path"].initial
= params
["kernel_path"]
1034 self
.fields
["nic_type"].initial
= params
["nic_type"]
1035 self
.fields
["root_path"].initial
= params
["root_path"]
1036 self
.fields
["serial_console"].initial
= params
["serial_console"]
1038 # Remove cdrom2 if the cluster doesn't have it; see #11655.
1039 if not has_cdrom2(cluster
):
1040 del self
.fields
["cdrom2_image_path"]
1042 def _configure_for_template(self
, template
):
1046 self
.fields
["kernel_path"].initial
= template
.kernel_path
1047 self
.fields
["root_path"].initial
= template
.root_path
1048 self
.fields
["serial_console"].initial
= template
.serial_console
1049 self
.fields
["boot_order"].initial
= template
.boot_order
1050 self
.fields
["cdrom_image_path"].initial
= template
.cdrom_image_path
1051 self
.fields
["cdrom2_image_path"].initial
= template
.cdrom2_image_path
1052 self
.fields
["disk_type"].initial
= template
.disk_type
1053 self
.fields
["nic_type"].initial
= template
.nic_type
1056 data
= super(VMWizardKVMForm
, self
).clean()
1058 # Force cdrom disk type to IDE; see #9297.
1059 data
['cdrom_disk_type'] = 'ide'
1061 # If booting from CD-ROM, require the first CD-ROM image to be
1063 if (data
.get("boot_order") == "cdrom" and
1064 not data
.get("cdrom_image_path")):
1065 msg
= u
"%s." % _("Image path required if boot device is CD-ROM")
1066 self
._errors
["cdrom_image_path"] = self
.error_class([msg
])
1071 class VMWizardView(LoginRequiredMixin
, CookieWizardView
):
1072 template_name
= "ganeti/forms/vm_wizard.html"
1075 # value, display value
1076 # value corresponds to VMWizardOwnerForm's fields
1077 ('template_name', 'Template'),
1078 ('hostname', 'Virtual Machine'),
1081 def _get_vm_or_template(self
):
1082 """Returns items that were not checked in step0"""
1083 data
= self
.get_cleaned_data_for_step('0')
1085 options
= [option
[0] for option
in self
.OPTIONS
]
1086 choices
= data
.get('choices', None)
1087 # which boxes weren't checked
1088 unchecked
= set(options
) - set(choices
)
1093 def _get_template(self
):
1094 name
= self
.kwargs
.get("template")
1096 return VirtualMachineTemplate
.objects
.get(template_name
=name
)
1099 def _get_cluster(self
):
1100 data
= self
.get_cleaned_data_for_step("0")
1102 return data
["cluster"]
1106 data
= self
.get_cleaned_data_for_step("2")
1111 def _get_disk_template(self
):
1112 data
= self
.get_cleaned_data_for_step("2")
1114 return data
["disk_template"]
1117 def get_form(self
, step
=None, data
=None, files
=None):
1118 s
= int(self
.steps
.current
) if step
is None else int(step
)
1119 initial
= self
.get_form_initial(s
)
1122 form
= VMWizardClusterForm(data
=data
, options
=self
.OPTIONS
,
1124 form
._configure
_for
_user
(self
.request
.user
)
1125 # XXX this should somehow become totally invalid if the user
1126 # doesn't have perms on the template.
1128 form
= VMWizardOwnerForm(data
=data
)
1129 form
._configure
_for
_cluster
(self
._get
_cluster
())
1130 form
._configure
_for
_template
(self
._get
_template
(),
1131 choices
=self
._get
_vm
_or
_template
())
1133 form
= VMWizardBasicsForm(data
=data
)
1134 form
._configure
_for
_cluster
(self
._get
_cluster
())
1135 form
._configure
_for
_template
(self
._get
_template
())
1137 form
= VMWizardAdvancedForm(data
=data
)
1138 form
._configure
_for
_cluster
(self
._get
_cluster
())
1139 form
._configure
_for
_template
(self
._get
_template
())
1140 form
._configure
_for
_disk
_template
(self
._get
_disk
_template
())
1142 cluster
= self
._get
_cluster
()
1148 form
= VMWizardKVMForm(data
=data
)
1149 elif hv
== "xen-pvm":
1150 form
= VMWizardPVMForm(data
=data
)
1151 elif hv
== "xen-hvm":
1152 form
= VMWizardHVMForm(data
=data
)
1155 form
._configure
_for
_cluster
(cluster
)
1156 form
._configure
_for
_template
(self
._get
_template
())
1160 form
= super(VMWizardView
, self
).get_form(step
, data
, files
)
1164 def get_context_data(self
, form
, **kwargs
):
1165 context
= super(VMWizardView
, self
).get_context_data(form
=form
,
1168 "cluster_form": self
.get_cleaned_data_for_step("0"),
1169 "owner_form": self
.get_cleaned_data_for_step("1"),
1170 "basics_form": self
.get_cleaned_data_for_step("2"),
1171 "advanced_form": self
.get_cleaned_data_for_step("3"),
1172 "hv_form": self
.get_cleaned_data_for_step("4"),
1174 context
["summary"] = summary
1178 def done(self
, forms
, template
=None, **kwargs
):
1180 Create a template. Optionally, bind a template to a VM instance
1181 created from the template. Optionally, name the template and save it.
1182 One or both of those is done depending on what the user has requested.
1185 # Hack: accepting kwargs in order to be able to work in several
1188 if template
is None:
1189 template
= VirtualMachineTemplate()
1191 template
= self
._get
_template
()
1193 user
= self
.request
.user
1195 cluster
= forms
[0].cleaned_data
["cluster"]
1196 owner
= forms
[1].cleaned_data
["owner"]
1198 template_name
= forms
[1].cleaned_data
["template_name"]
1199 hostname
= forms
[1].cleaned_data
["hostname"]
1201 # choice_data are the options that were not checked
1202 # if unchecked, than we should make sure that this is not submitted.
1203 # this fixes cases where the user checked a box in the beginning, put
1204 # data into the input, and went back and unchecked that box later.
1205 unchecked_options
= self
._get
_vm
_or
_template
()
1206 for unchecked
in unchecked_options
:
1207 if 'template_name' == unchecked
:
1209 if 'hostname' == unchecked
:
1212 template
.cluster
= cluster
1213 template
.memory
= forms
[2].cleaned_data
["memory"]
1214 if has_balloonmem(cluster
):
1215 template
.minmem
= forms
[2].cleaned_data
["minram"]
1216 template
.vcpus
= forms
[2].cleaned_data
["vcpus"]
1217 template
.disk_template
= forms
[2].cleaned_data
["disk_template"]
1219 template
.disks
= forms
[2].cleaned_data
["disks"]
1221 nics
= forms
[2].cleaned_data
["nics"]
1224 nics
= [{"link": "br0", "mode": "bridged"}]
1225 template
.nics
= nics
1227 template
.os
= forms
[2].cleaned_data
["os"]
1228 template
.ip_check
= forms
[3].cleaned_data
["ip_check"]
1229 template
.name_check
= forms
[3].cleaned_data
["name_check"]
1230 template
.pnode
= forms
[3].cleaned_data
["pnode"].hostname
1232 hvparams
= forms
[4].cleaned_data
1234 template
.boot_order
= hvparams
.get("boot_order")
1235 template
.cdrom2_image_path
= hvparams
.get("cdrom2_image_path")
1236 template
.cdrom_image_path
= hvparams
.get("cdrom_image_path")
1237 template
.kernel_path
= hvparams
.get("kernel_path")
1238 template
.root_path
= hvparams
.get("root_path")
1239 template
.serial_console
= hvparams
.get("serial_console")
1241 if "snode" in forms
[3].cleaned_data
:
1242 template
.snode
= forms
[3].cleaned_data
["snode"].hostname
1244 template
.set_name(template_name
)
1245 # only save the template to the database if its not temporary
1246 if not template
.temporary
:
1250 vm
= template_to_instance(template
, hostname
, owner
)
1251 log_action('CREATE', user
, vm
)
1252 return HttpResponseRedirect(reverse('instance-detail',
1256 return HttpResponseRedirect(reverse("template-detail",
1261 def vm_wizard(*args
, **kwargs
):
1263 VMWizardClusterForm
,
1266 VMWizardAdvancedForm
,
1269 initial
= kwargs
.get('initial_dict', None)
1270 return VMWizardView
.as_view(forms
, initial_dict
=initial
)