213
234
return all_architectures[0]
237
def clean_distro_series_field(form, field, os_field):
238
"""Cleans the distro_series field in the form. Validating that
239
the selected operating system matches the distro_series.
241
:param form: `Form` class
242
:param field: distro_series field name
243
:param os_field: osystem field name
244
:returns: clean distro_series field value
246
new_distro_series = form.cleaned_data.get(field)
247
if '*' in new_distro_series:
248
new_distro_series = new_distro_series.replace('*', '')
249
if new_distro_series is None or '/' not in new_distro_series:
250
return new_distro_series
251
os, release = new_distro_series.split('/', 1)
252
if os_field in form.cleaned_data:
253
new_os = form.cleaned_data[os_field]
255
raise ValidationError(
256
"%s in %s does not match with "
257
"operating system %s" % (release, field, os))
261
def get_osystem_from_release(release):
262
"""Returns the operating system that supports that release."""
263
for _, osystem in OperatingSystemRegistry:
264
if release in osystem.get_supported_releases():
216
269
class NodeForm(ModelForm):
218
271
def __init__(self, request=None, *args, **kwargs):
244
298
choices=choices, required=True, initial=default_arch,
245
299
error_messages={'invalid_choice': invalid_arch_message})
301
def set_up_osystem_and_distro_series_fields(self, instance):
302
"""Create the `osystem` and `distro_series` fields.
304
This needs to be done on the fly so that we can pass a dynamic list of
305
usable operating systems and distro_series.
307
osystems = list_all_usable_osystems()
308
releases = list_all_usable_releases(osystems)
309
os_choices = list_osystem_choices(osystems)
310
distro_choices = list_release_choices(releases)
311
invalid_osystem_message = compose_invalid_choice_text(
312
'osystem', os_choices)
313
invalid_distro_series_message = compose_invalid_choice_text(
314
'distro_series', distro_choices)
315
self.fields['osystem'] = forms.ChoiceField(
316
label="OS", choices=os_choices, required=False, initial='',
317
error_messages={'invalid_choice': invalid_osystem_message})
318
self.fields['distro_series'] = forms.ChoiceField(
319
label="Release", choices=distro_choices,
320
required=False, initial='',
321
error_messages={'invalid_choice': invalid_distro_series_message})
322
if instance is not None:
323
initial_value = get_distro_series_initial(instance)
324
if instance is not None:
325
self.initial['distro_series'] = initial_value
247
327
def clean_hostname(self):
248
328
# Don't allow the hostname to be changed if the node is
249
329
# currently allocated. Juju knows the node by its old name, so
268
distro_series = forms.ChoiceField(
269
choices=DISTRO_SERIES_CHOICES, required=False,
270
initial=DISTRO_SERIES.default,
272
error_messages={'invalid_choice': INVALID_DISTRO_SERIES_MESSAGE})
351
def clean_license_key(self):
352
key = self.cleaned_data.get('license_key')
353
osystem = self.cleaned_data.get('osystem')
354
distro = self.cleaned_data.get('distro_series')
356
os_obj = OperatingSystemRegistry.get_item(osystem)
357
if os_obj is not None and os_obj.requires_license_key(distro):
358
if not key or len(key) == 0:
359
raise ValidationError(
360
"This OS/Release requires a license_key")
361
if not os_obj.validate_license_key(distro, key):
362
raise ValidationError(
363
"Invalid license key.")
367
def set_distro_series(self, series=''):
368
"""Sets the osystem and distro_series, from the provided
371
# This implementation is used so that current API, is not broken. This
372
# makes the distro_series a flat namespace. The distro_series is used
373
# to search through the supporting operating systems, to find the
374
# correct operating system that supports this distro_series.
376
self.data['osystem'] = ''
377
self.data['distro_series'] = ''
378
if series is not None and series != '':
379
osystem = get_osystem_from_release(series)
380
if osystem is not None:
381
key_required = get_release_requires_key(osystem, series)
382
self.data['osystem'] = osystem.name
383
self.data['distro_series'] = '%s/%s%s' % (
389
self.data['distro_series'] = series
391
def set_license_key(self, license_key=''):
392
"""Sets the license key."""
394
self.data['license_key'] = license_key
274
396
hostname = forms.CharField(
275
397
label="Host name", required=False, help_text=(
839
1016
"""Settings page, Commissioning section."""
840
1017
check_compatibility = get_config_field('check_compatibility')
841
1018
commissioning_distro_series = forms.ChoiceField(
842
choices=COMMISSIONING_DISTRO_SERIES_CHOICES, required=False,
843
label="Default distro series used for commissioning",
1019
choices=list_commisioning_choices(), required=False,
1020
label="Default Ubuntu release used for commissioning",
844
1021
error_messages={'invalid_choice': compose_invalid_choice_text(
845
1022
'commissioning_distro_series',
846
COMMISSIONING_DISTRO_SERIES_CHOICES)})
1023
list_commisioning_choices())})
1026
class DeployForm(ConfigForm):
1027
"""Settings page, Deploy section."""
1029
def __init__(self, *args, **kwargs):
1030
Form.__init__(self, *args, **kwargs)
1031
self.fields['default_osystem'] = get_config_field('default_osystem')
1032
self.fields['default_distro_series'] = get_config_field(
1033
'default_distro_series')
1034
self._load_initials()
1036
def _load_initials(self):
1037
super(DeployForm, self)._load_initials()
1038
initial_os = self.fields['default_osystem'].initial
1039
initial_series = self.fields['default_distro_series'].initial
1040
self.initial['default_distro_series'] = '%s/%s' % (
1045
def clean_default_distro_series(self):
1046
return clean_distro_series_field(
1047
self, 'default_distro_series', 'default_osystem')
849
1050
class UbuntuForm(ConfigForm):
850
1051
"""Settings page, Ubuntu section."""
851
default_distro_series = get_config_field('default_distro_series')
852
1052
main_archive = get_config_field('main_archive')
853
1053
ports_archive = get_config_field('ports_archive')
1056
class WindowsForm(ConfigForm):
1057
"""Settings page, Windows section."""
1058
windows_kms_host = get_config_field('windows_kms_host')
856
1061
class GlobalKernelOptsForm(ConfigForm):
857
1062
"""Settings page, Global Kernel Parameters section."""
858
1063
kernel_opts = get_config_field('kernel_opts')
1066
ERROR_MESSAGE_STATIC_IPS_OUTSIDE_RANGE = (
1067
"New static IP range does not include already-allocated IP "
1071
ERROR_MESSAGE_STATIC_RANGE_IN_USE = (
1072
"Cannot remove static IP range when there are allocated IP addresses "
1076
def validate_new_static_ip_ranges(instance, static_ip_range_low,
1077
static_ip_range_high):
1078
"""Check that new static IP ranges don't exclude allocated addresses.
1080
If there are IP addresses allocated within a `NodeGroupInterface`'s
1081
existing static IP range which would fall outside of the new range,
1082
raise a ValidationError.
1084
# Return early if the instance is not already managed, it currently
1085
# has no static IP range, or the static IP range hasn't changed.
1086
if not instance.is_managed:
1088
# Deliberately vague check to allow for empty strings.
1089
if (not instance.static_ip_range_low or
1090
not instance.static_ip_range_high):
1092
if (static_ip_range_low == instance.static_ip_range_low and
1093
static_ip_range_high == instance.static_ip_range_high):
1096
cursor = connection.cursor()
1098
# Deliberately vague check to allow for empty strings.
1099
if static_ip_range_low or static_ip_range_high:
1100
# Find any allocated addresses within the old static range which do
1101
# not fall within the *new* static range. This means that we allow
1102
# for range expansion and contraction *unless* that means dropping
1103
# IP addresses that are already allocated.
1105
SELECT TRUE FROM maasserver_staticipaddress
1106
WHERE ip >= %s AND ip <= %s
1107
AND (ip < %s OR ip > %s)
1109
instance.static_ip_range_low,
1110
instance.static_ip_range_high,
1111
static_ip_range_low,
1112
static_ip_range_high))
1113
results = cursor.fetchall()
1115
raise forms.ValidationError(
1116
ERROR_MESSAGE_STATIC_IPS_OUTSIDE_RANGE)
1118
# Check that there's no IP addresses allocated in the old range;
1119
# if there are, we can't remove the range yet.
1121
SELECT TRUE FROM maasserver_staticipaddress
1122
WHERE ip >= %s AND ip <= %s
1124
instance.static_ip_range_low,
1125
instance.static_ip_range_high))
1126
results = cursor.fetchall()
1128
raise forms.ValidationError(
1129
ERROR_MESSAGE_STATIC_RANGE_IN_USE)
1133
def create_Network_from_NodeGroupInterface(interface):
1134
"""Given a `NodeGroupInterface`, create its Network counterpart."""
1135
# This method cannot use non-orm model properties because it needs
1136
# to be used in a data migration, where they won't work.
1137
if not interface.subnet_mask:
1138
# Can be None or empty string, do nothing if so.
1141
name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)
1142
ipnetwork = make_network(interface.ip, interface.subnet_mask)
1145
ip=unicode(ipnetwork.network),
1146
netmask=unicode(ipnetwork.netmask),
1149
"Auto created when creating interface %s on cluster "
1150
"%s" % (interface.interface, interface.nodegroup.name)),
1154
except (IntegrityError, ValidationError):
1155
# It probably already exists, keep calm and carry on.
861
1160
class NodeGroupInterfaceForm(ModelForm):
863
1162
management = forms.TypedChoiceField(
1529
1866
def save(self):
1530
1867
"""Disconnect the MAC addresses from the form's network."""
1531
1868
self.network.macaddress_set.remove(*self.get_macs())
1871
class BootSourceForm(ModelForm):
1872
"""Form for the Boot Source API."""
1882
keyring_filename = forms.CharField(
1883
label="The path to the keyring file for this BootSource.",
1886
keyring_data = forms.FileField(
1887
label="The GPG keyring for this BootSource, as a binary blob.",
1890
def __init__(self, nodegroup=None, **kwargs):
1891
super(BootSourceForm, self).__init__(**kwargs)
1892
if 'instance' in kwargs:
1893
self.nodegroup = kwargs['instance'].cluster
1895
self.nodegroup = nodegroup
1897
def clean_keyring_data(self):
1898
"""Process 'keyring_data' field.
1900
Return the InMemoryUploadedFile's content so that it can be
1901
stored in the boot source's 'keyring_data' binary field.
1903
data = self.cleaned_data.get('keyring_data', None)
1904
if data is not None:
1908
def save(self, *args, **kwargs):
1909
boot_source = super(BootSourceForm, self).save(commit=False)
1910
boot_source.cluster = self.nodegroup
1911
if kwargs.get('commit', True):
1912
boot_source.save(*args, **kwargs)
1916
class BootSourceSelectionForm(ModelForm):
1917
"""Form for the Boot Source Selection API."""
1920
model = BootSourceSelection
1928
# Use UnconstrainedMultipleChoiceField fields for multiple-choices
1929
# fields instead of the default (djorm-ext-pgarray's ArrayFormField):
1930
# ArrayFormField deals with comma-separated lists and here we want to
1931
# handle multiple-values submissions.
1932
arches = UnconstrainedMultipleChoiceField(label="Architecture list")
1933
subarches = UnconstrainedMultipleChoiceField(label="Subarchitecture list")
1934
labels = UnconstrainedMultipleChoiceField(label="Label list")
1936
def __init__(self, boot_source=None, **kwargs):
1937
super(BootSourceSelectionForm, self).__init__(**kwargs)
1938
if 'instance' in kwargs:
1939
self.boot_source = kwargs['instance'].boot_source
1941
self.boot_source = boot_source
1943
def save(self, *args, **kwargs):
1944
boot_source_selection = super(
1945
BootSourceSelectionForm, self).save(commit=False)
1946
boot_source_selection.boot_source = self.boot_source
1947
if kwargs.get('commit', True):
1948
boot_source_selection.save(*args, **kwargs)
1949
return boot_source_selection
1952
class LicenseKeyForm(ModelForm):
1953
"""Form for global license keys."""
1963
def __init__(self, *args, **kwargs):
1964
super(LicenseKeyForm, self).__init__(*args, **kwargs)
1965
self.set_up_osystem_and_distro_series_fields(kwargs.get('instance'))
1967
def set_up_osystem_and_distro_series_fields(self, instance):
1968
"""Create the `osystem` and `distro_series` fields.
1970
This needs to be done on the fly so that we can pass a dynamic list of
1971
usable operating systems and distro_series.
1973
osystems = list_all_usable_osystems(have_images=False)
1974
releases = list_all_releases_requiring_keys(osystems)
1976
# Remove the operating systems that do not have any releases that
1977
# require license keys. Don't want them to show up in the UI or be
1981
for osystem in osystems
1982
if osystem.name in releases
1985
os_choices = list_osystem_choices(osystems, include_default=False)
1986
distro_choices = list_release_choices(
1987
releases, include_default=False, include_latest=False,
1988
with_key_required=False)
1989
invalid_osystem_message = compose_invalid_choice_text(
1990
'osystem', os_choices)
1991
invalid_distro_series_message = compose_invalid_choice_text(
1992
'distro_series', distro_choices)
1993
self.fields['osystem'] = forms.ChoiceField(
1994
label="OS", choices=os_choices, required=True,
1995
error_messages={'invalid_choice': invalid_osystem_message})
1996
self.fields['distro_series'] = forms.ChoiceField(
1997
label="Release", choices=distro_choices, required=True,
1998
error_messages={'invalid_choice': invalid_distro_series_message})
1999
if instance is not None:
2000
initial_value = get_distro_series_initial(
2001
instance, with_key_required=False)
2002
if instance is not None:
2003
self.initial['distro_series'] = initial_value
2005
def full_clean(self):
2006
# When this form is used from the API, the distro_series field will
2007
# not be formatted correctly. This is to make it easy on the user, and
2008
# not have to call the api with distro_series=os/series. This occurs
2009
# in full_clean, so the value is correct before validation occurs on
2010
# the distro_series field.
2011
if 'distro_series' in self.data and 'osystem' in self.data:
2012
if '/' not in self.data['distro_series']:
2013
self.data['distro_series'] = '%s/%s' % (
2014
self.data['osystem'],
2015
self.data['distro_series'],
2017
super(LicenseKeyForm, self).full_clean()
2020
"""Validate distro_series and osystem match, and license_key is valid
2021
for selected operating system and series."""
2022
# Get the clean_data, check that all of the fields we need are
2023
# present. If not then the form will error, so no reason to continue.
2024
cleaned_data = super(LicenseKeyForm, self).clean()
2025
required_fields = ['license_key', 'osystem', 'distro_series']
2026
for field in required_fields:
2027
if field not in cleaned_data:
2029
cleaned_data['distro_series'] = self.clean_osystem_distro_series_field(
2031
self.validate_license_key(cleaned_data)
2034
def clean_osystem_distro_series_field(self, cleaned_data):
2035
"""Validate that os/distro_series matches osystem, and update the
2036
distro_series field, to remove the leading os/."""
2037
cleaned_osystem = cleaned_data['osystem']
2038
cleaned_series = cleaned_data['distro_series']
2039
series_os, release = cleaned_series.split('/', 1)
2040
if series_os != cleaned_osystem:
2041
raise ValidationError(
2042
"%s in distro_series does not match with "
2043
"operating system %s" % (release, cleaned_osystem))
2046
def validate_license_key(self, cleaned_data):
2047
"""Validates that the license key is valid."""
2048
cleaned_key = cleaned_data['license_key']
2049
cleaned_osystem = cleaned_data['osystem']
2050
cleaned_series = cleaned_data['distro_series']
2051
os_obj = OperatingSystemRegistry.get_item(cleaned_osystem)
2053
raise ValidationError(
2054
"Failed to retrieve %s from os registry." % cleaned_osystem)
2055
elif not os_obj.validate_license_key(cleaned_series, cleaned_key):
2056
raise ValidationError("Invalid license key.")