384
387
# Are we creating a new node object?
385
388
self.new_node = (instance is None)
387
self.set_up_architecture_field()
389
self.set_up_osystem_and_distro_series_fields(instance)
391
390
self.fields['disable_ipv4'] = forms.BooleanField(
392
391
label="", required=False)
394
# We only want the license key field to render in the UI if the `OS`
395
# and `Release` fields are also present.
397
self.fields['license_key'] = forms.CharField(
398
label="License Key", required=False, help_text=(
399
"License key for operating system"),
402
self.fields['license_key'] = forms.CharField(
403
label="", required=False, widget=forms.HiddenInput())
405
def set_up_architecture_field(self):
406
"""Create the `architecture` field.
408
This needs to be done on the fly so that we can pass a dynamic list of
409
usable architectures.
411
architectures = list_all_usable_architectures()
412
default_arch = pick_default_architecture(architectures)
413
if len(architectures) == 0:
414
choices = [BLANK_CHOICE]
416
choices = list_architecture_choices(architectures)
417
invalid_arch_message = compose_invalid_choice_text(
418
'architecture', choices)
419
self.fields['architecture'] = forms.ChoiceField(
420
choices=choices, required=False, initial=default_arch,
421
error_messages={'invalid_choice': invalid_arch_message})
423
def set_up_osystem_and_distro_series_fields(self, instance):
424
"""Create the `osystem` and `distro_series` fields.
426
This needs to be done on the fly so that we can pass a dynamic list of
427
usable operating systems and distro_series.
429
osystems = list_all_usable_osystems()
430
releases = list_all_usable_releases(osystems)
432
os_choices = list_osystem_choices(osystems)
433
distro_choices = list_release_choices(releases)
434
invalid_osystem_message = compose_invalid_choice_text(
435
'osystem', os_choices)
436
invalid_distro_series_message = compose_invalid_choice_text(
437
'distro_series', distro_choices)
438
self.fields['osystem'] = forms.ChoiceField(
439
label="OS", choices=os_choices, required=False, initial='',
440
error_messages={'invalid_choice': invalid_osystem_message})
441
self.fields['distro_series'] = forms.ChoiceField(
442
label="Release", choices=distro_choices,
443
required=False, initial='',
445
'invalid_choice': invalid_distro_series_message})
447
self.fields['osystem'] = forms.ChoiceField(
448
label="", required=False, widget=forms.HiddenInput())
449
self.fields['distro_series'] = forms.ChoiceField(
450
label="", required=False, widget=forms.HiddenInput())
451
if instance is not None:
452
initial_value = get_distro_series_initial(osystems, instance)
453
if instance is not None:
454
self.initial['distro_series'] = initial_value
456
def clean_distro_series(self):
457
return clean_distro_series_field(self, 'distro_series', 'osystem')
459
393
def clean_disable_ipv4(self):
460
394
# Boolean fields only show up in UI form submissions as "true" (if the
461
395
# box was checked) or not at all (if the box was not checked). This
495
429
except ValueError:
496
430
raise ValidationError('Invalid size for swap: %s' % swap_size)
432
def clean_domain(self):
433
domain = self.cleaned_data.get('domain')
437
return Domain.objects.get(id=int(domain))
440
return Domain.objects.get(name=domain)
441
except Domain.DoesNotExist:
442
raise ValidationError("Unable to find domain %s" % domain)
444
hostname = forms.CharField(
445
label="Host name", required=False, help_text=(
446
"The hostname of the machine"))
448
domain = forms.CharField(
449
label="Domain name", required=False, help_text=(
450
"The domain name of the machine."))
452
swap_size = forms.CharField(
453
label="Swap size", required=False, help_text=(
454
"The size of the swap file in bytes. The field also accepts K, M, "
455
"G and T meaning kilobytes, megabytes, gigabytes and terabytes."))
460
# Fields that the form should generate automatically from the
462
# Note: fields have to be added here even if they were defined manually
463
# elsewhere in the form
472
class MachineForm(NodeForm):
473
def __init__(self, request=None, *args, **kwargs):
474
super(MachineForm, self).__init__(*args, **kwargs)
476
# Even though it doesn't need it and doesn't use it, this form accepts
477
# a parameter named 'request' because it is used interchangingly
478
# with AdminMachineForm which actually uses this parameter.
479
instance = kwargs.get('instance')
481
self.set_up_architecture_field()
482
# We only want the license key field to render in the UI if the `OS`
483
# and `Release` fields are also present.
485
self.set_up_osystem_and_distro_series_fields(instance)
486
self.fields['license_key'] = forms.CharField(
487
label="License Key", required=False, help_text=(
488
"License key for operating system"),
491
self.fields['license_key'] = forms.CharField(
492
label="", required=False, widget=forms.HiddenInput())
494
def set_up_architecture_field(self):
495
"""Create the `architecture` field.
497
This needs to be done on the fly so that we can pass a dynamic list of
498
usable architectures.
500
architectures = list_all_usable_architectures()
501
default_arch = pick_default_architecture(architectures)
502
if len(architectures) == 0:
503
choices = [BLANK_CHOICE]
505
choices = list_architecture_choices(architectures)
506
invalid_arch_message = compose_invalid_choice_text(
507
'architecture', choices)
508
self.fields['architecture'] = forms.ChoiceField(
509
choices=choices, required=False, initial=default_arch,
510
error_messages={'invalid_choice': invalid_arch_message})
512
def set_up_osystem_and_distro_series_fields(self, instance):
513
"""Create the `osystem` and `distro_series` fields.
515
This needs to be done on the fly so that we can pass a dynamic list of
516
usable operating systems and distro_series.
518
osystems = list_all_usable_osystems()
519
releases = list_all_usable_releases(osystems)
521
os_choices = list_osystem_choices(osystems)
522
distro_choices = list_release_choices(releases)
523
invalid_osystem_message = compose_invalid_choice_text(
524
'osystem', os_choices)
525
invalid_distro_series_message = compose_invalid_choice_text(
526
'distro_series', distro_choices)
527
self.fields['osystem'] = forms.ChoiceField(
528
label="OS", choices=os_choices, required=False, initial='',
529
error_messages={'invalid_choice': invalid_osystem_message})
530
self.fields['distro_series'] = forms.ChoiceField(
531
label="Release", choices=distro_choices,
532
required=False, initial='',
534
'invalid_choice': invalid_distro_series_message})
536
self.fields['osystem'] = forms.ChoiceField(
537
label="", required=False, widget=forms.HiddenInput())
538
self.fields['distro_series'] = forms.ChoiceField(
539
label="", required=False, widget=forms.HiddenInput())
540
if instance is not None:
541
initial_value = get_distro_series_initial(osystems, instance)
542
if instance is not None:
543
self.initial['distro_series'] = initial_value
545
def clean_distro_series(self):
546
return clean_distro_series_field(self, 'distro_series', 'osystem')
498
548
def clean_min_hwe_kernel(self):
499
549
min_hwe_kernel = self.cleaned_data.get('min_hwe_kernel')
500
550
if self.new_node and not min_hwe_kernel:
585
635
self.is_bound = True
586
636
self.data['hwe_kernel'] = hwe_kernel
588
hostname = forms.CharField(
589
label="Host name", required=False, help_text=(
590
"The FQDN (Fully Qualified Domain Name) is derived from the "
591
"host name: If the cluster controller for this node is managing "
592
"DNS then the domain part in the host name (if any) is replaced "
593
"by the domain defined on the cluster; if the cluster controller "
594
"does not manage DNS, then the host name as entered will be the "
597
swap_size = forms.CharField(
598
label="Swap size", required=False, help_text=(
599
"The size of the swap file in bytes. The field also accepts K, M, "
600
"G and T meaning kilobytes, megabytes, gigabytes and terabytes."))
605
# Fields that the form should generate automatically from the
607
# Note: fields have to be added here even if they were defined manually
608
# elsewhere in the form
641
fields = NodeForm.Meta.fields + (
617
646
'min_hwe_kernel',
622
class DeviceForm(MAASModelForm):
651
class DeviceForm(NodeForm):
623
652
parent = forms.ModelChoiceField(
624
653
required=False, initial=None,
625
654
queryset=Node.objects.all(), to_field_name='system_id')
718
743
if instance is not None:
719
744
self.initial['zone'] = instance.zone.name
746
def save(self, *args, **kwargs):
747
"""Persist the node into the database."""
748
node = super(AdminNodeForm, self).save(commit=False)
749
zone = self.cleaned_data.get('zone')
752
if kwargs.get('commit', True):
753
node.save(*args, **kwargs)
754
self.save_m2m() # Save many to many relations.
758
class AdminMachineForm(MachineForm, AdminNodeForm):
759
"""A `MachineForm` which includes fields that only an admin may change."""
764
# Fields that the form should generate automatically from the
766
fields = MachineForm.Meta.fields + (
771
def __init__(self, data=None, instance=None, request=None, **kwargs):
772
super(AdminMachineForm, self).__init__(
773
data=data, instance=instance, **kwargs)
774
AdminMachineForm.set_up_power_type(self, data, instance)
722
def _get_power_type(form, data, node):
777
def _get_power_type(form, data, machine):
726
781
power_type = data.get('power_type', form.initial.get('power_type'))
728
# If power_type is None (this is a node creation form or this
783
# If power_type is None (this is a machine creation form or this
729
784
# form deals with an API call which does not change the value of
730
# 'power_type') or invalid: get the node's current 'power_type'
731
# value or the default value if this form is not linked to a node.
785
# 'power_type') or invalid: get the machine's current 'power_type'
786
# value or the default value if this form is not linked to a machine.
733
788
power_types = get_power_types()
734
789
except ClusterUnavailable as e:
743
798
if power_type not in power_types:
744
return '' if node is None else node.power_type
799
return '' if machine is None else machine.power_type
745
800
return power_type
748
def set_up_power_type(form, data, node=None):
803
def set_up_power_type(form, data, machine=None):
749
804
"""Set up the 'power_type' and 'power_parameters' fields.
751
806
This can't be done at the model level because the choices need to
752
807
be generated on the fly by get_power_type_choices().
754
power_type = AdminNodeForm._get_power_type(form, data, node)
809
power_type = AdminMachineForm._get_power_type(form, data, machine)
755
810
choices = [BLANK_CHOICE] + get_power_type_choices()
756
811
form.fields['power_type'] = forms.ChoiceField(
757
812
required=False, choices=choices, initial=power_type)
782
837
return cleaned_data
785
cleaned_data = super(AdminNodeForm, self).clean()
786
return AdminNodeForm.check_power_type(self, cleaned_data)
840
cleaned_data = super(AdminMachineForm, self).clean()
841
return AdminMachineForm.check_power_type(self, cleaned_data)
789
def set_power_type(form, node):
790
"""Persist the node into the database."""
844
def set_power_type(form, machine):
845
"""Persist the machine into the database."""
791
846
power_type = form.cleaned_data.get('power_type')
792
847
if power_type is not None:
793
node.power_type = power_type
848
machine.power_type = power_type
794
849
power_parameters = form.cleaned_data.get('power_parameters')
795
850
if power_parameters is not None:
796
node.power_parameters = power_parameters
851
machine.power_parameters = power_parameters
798
853
def save(self, *args, **kwargs):
799
854
"""Persist the node into the database."""
800
node = super(AdminNodeForm, self).save(commit=False)
855
machine = super(AdminMachineForm, self).save(commit=False)
801
856
zone = self.cleaned_data.get('zone')
804
AdminNodeForm.set_power_type(self, node)
859
AdminMachineForm.set_power_type(self, machine)
805
860
if kwargs.get('commit', True):
806
node.save(*args, **kwargs)
861
machine.save(*args, **kwargs)
807
862
self.save_m2m() # Save many to many relations.
866
def get_machine_edit_form(user):
867
if user.is_superuser:
868
return AdminMachineForm
811
873
def get_node_edit_form(user):
1008
1070
"""A form mixin which dynamically adds power_type and power_parameters to
1009
1071
the list of fields. This mixin also overrides the 'save' method to persist
1010
1072
these fields and is intended to be used with a class inheriting from
1014
1076
def __init__(self, *args, **kwargs):
1015
1077
super(WithPowerMixin, self).__init__(*args, **kwargs)
1016
1078
self.data = self.data.copy()
1017
AdminNodeForm.set_up_power_type(self, self.data)
1079
AdminMachineForm.set_up_power_type(self, self.data)
1019
1081
def clean(self):
1020
1082
cleaned_data = super(WithPowerMixin, self).clean()
1021
return AdminNodeForm.check_power_type(self, cleaned_data)
1083
return AdminMachineForm.check_power_type(self, cleaned_data)
1023
1085
def save(self, *args, **kwargs):
1024
1086
"""Persist the node into the database."""
1025
1087
node = super(WithPowerMixin, self).save()
1026
AdminNodeForm.set_power_type(self, node)
1088
AdminMachineForm.set_power_type(self, node)
1031
class AdminNodeWithMACAddressesForm(WithMACAddressesMixin, AdminNodeForm):
1032
"""A version of the AdminNodeForm which includes the multi-MAC address
1093
class AdminMachineWithMACAddressesForm(
1094
WithMACAddressesMixin, AdminMachineForm):
1095
"""A version of the AdminMachineForm which includes the multi-MAC address
1037
class NodeWithMACAddressesForm(WithMACAddressesMixin, NodeForm):
1038
"""A version of the NodeForm which includes the multi-MAC address field.
1100
class MachineWithMACAddressesForm(WithMACAddressesMixin, MachineForm):
1101
"""A version of the MachineForm which includes the multi-MAC address field.
1042
class NodeWithPowerAndMACAddressesForm(
1043
WithPowerMixin, NodeWithMACAddressesForm):
1044
"""A version of the NodeForm which includes the power fields.
1105
class MachineWithPowerAndMACAddressesForm(
1106
WithPowerMixin, MachineWithMACAddressesForm):
1107
"""A version of the MachineForm which includes the power fields.
2291
2354
return self.block_device
2294
class MountFilesystemForm(Form):
2295
"""Form used to mount a filesystem."""
2298
def __init__(self, filesystem: Optional[Filesystem], *args, **kwargs):
2299
super(MountFilesystemForm, self).__init__(*args, **kwargs)
2300
self.filesystem = filesystem
2304
if self.filesystem is not None:
2305
if self.filesystem.uses_mount_point:
2306
self.fields["mount_point"] = AbsolutePathField(required=True)
2307
self.fields["mount_options"] = StrippedCharField(required=False)
2310
cleaned_data = super(MountFilesystemForm, self).clean()
2311
if self.filesystem is None:
2313
None, "Cannot mount an unformatted partition "
2315
elif self.filesystem.filesystem_group is not None:
2317
None, "Filesystem is part of a filesystem group, "
2318
"and cannot be mounted.")
2322
if "mount_point" in self.cleaned_data:
2323
self.filesystem.mount_point = self.cleaned_data['mount_point']
2325
self.filesystem.mount_point = "none" # e.g. for swap.
2326
if "mount_options" in self.cleaned_data:
2327
self.filesystem.mount_options = self.cleaned_data['mount_options']
2328
self.filesystem.save()
2331
2357
class AddPartitionForm(Form):
2332
2358
"""Form used to add a partition to block device."""