diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 2cfbbcc70..a7ef02396 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -401,8 +401,11 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): if top_device: min_height = top_device.position + top_device.device_type.u_height - 1 if self.u_height < min_height: - raise ValidationError("Rack must be at least {}U tall with currently installed devices." - .format(min_height)) + raise ValidationError({ + 'u_height': "Rack must be at least {}U tall to house currently installed devices.".format( + min_height + ) + }) def to_csv(self): return ','.join([ @@ -596,27 +599,39 @@ class DeviceType(models.Model): u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required, exclude=[d.pk]) if d.position not in u_available: - raise ValidationError("Device {} in rack {} does not have sufficient space to accommodate a height " - "of {}U".format(d, d.rack, self.u_height)) + raise ValidationError({ + 'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of " + "{}U".format(d, d.rack, self.u_height) + }) if not self.is_console_server and self.cs_port_templates.count(): - raise ValidationError("Must delete all console server port templates associated with this device before " - "declassifying it as a console server.") + raise ValidationError({ + 'is_console_server': "Must delete all console server port templates associated with this device before " + "declassifying it as a console server." + }) if not self.is_pdu and self.power_outlet_templates.count(): - raise ValidationError("Must delete all power outlet templates associated with this device before " - "declassifying it as a PDU.") + raise ValidationError({ + 'is_pdu': "Must delete all power outlet templates associated with this device before declassifying it " + "as a PDU." + }) if not self.is_network_device and self.interface_templates.filter(mgmt_only=False).count(): - raise ValidationError("Must delete all non-management-only interface templates associated with this device " - "before declassifying it as a network device.") + raise ValidationError({ + 'is_network_device': "Must delete all non-management-only interface templates associated with this " + "device before declassifying it as a network device." + }) if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count(): - raise ValidationError("Must delete all device bay templates associated with this device before " - "declassifying it as a parent device.") + raise ValidationError({ + 'subdevice_role': "Must delete all device bay templates associated with this device before " + "declassifying it as a parent device." + }) if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD: - raise ValidationError("Child device types must be 0U.") + raise ValidationError({ + 'u_height': "Child device types must be 0U." + }) @property def is_parent_device(self): @@ -824,29 +839,39 @@ class Device(CreatedUpdatedModel, CustomFieldModel): def clean(self): - # Validate device type assignment - if not hasattr(self, 'device_type'): - raise ValidationError("Must specify device type.") - - # Child devices cannot be assigned to a rack face/unit - if self.device_type.is_child_device and (self.face is not None or self.position): - raise ValidationError("Child device types cannot be assigned a rack face or position.") - # Validate position/face combination if self.position and self.face is None: - raise ValidationError("Must specify rack face with rack position.") + raise ValidationError({ + 'face': "Must specify rack face when defining rack position." + }) - # Validate rack space - rack_face = self.face if not self.device_type.is_full_depth else None - exclude_list = [self.pk] if self.pk else [] - try: - available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face, - exclude=exclude_list) - if self.position and self.position not in available_units: - raise ValidationError("U{} is already occupied or does not have sufficient space to accommodate a(n) " - "{} ({}U).".format(self.position, self.device_type, self.device_type.u_height)) - except Rack.DoesNotExist: - pass + if self.device_type: + + # Child devices cannot be assigned to a rack face/unit + if self.device_type.is_child_device and self.face is not None: + raise ValidationError({ + 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent " + "device." + }) + if self.device_type.is_child_device and self.position: + raise ValidationError({ + 'position': "Child device types cannot be assigned to a rack position. This is an attribute of the " + "parent device." + }) + + # Validate rack space + rack_face = self.face if not self.device_type.is_full_depth else None + exclude_list = [self.pk] if self.pk else [] + try: + available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face, + exclude=exclude_list) + if self.position and self.position not in available_units: + raise ValidationError({ + 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} " + "({}U).".format(self.position, self.device_type, self.device_type.u_height) + }) + except Rack.DoesNotExist: + pass def save(self, *args, **kwargs): @@ -1094,9 +1119,10 @@ class Interface(models.Model): def clean(self): if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected: - raise ValidationError({'form_factor': "Virtual interfaces cannot be connected to another interface or " - "circuit. Disconnect the interface or choose a physical form " - "factor."}) + raise ValidationError({ + 'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the " + "interface or choose a physical form factor." + }) @property def is_physical(self): @@ -1147,7 +1173,9 @@ class InterfaceConnection(models.Model): def clean(self): if self.interface_a == self.interface_b: - raise ValidationError("Cannot connect an interface to itself") + raise ValidationError({ + 'interface_b': "Cannot connect an interface to itself." + }) # Used for connections export def to_csv(self): @@ -1180,8 +1208,9 @@ class DeviceBay(models.Model): # Validate that the parent Device can have DeviceBays if not self.device.device_type.is_parent_device: - raise ValidationError("This type of device ({}) does not support device bays." - .format(self.device.device_type)) + raise ValidationError("This type of device ({}) does not support device bays.".format( + self.device.device_type + )) # Cannot install a device into itself, obviously if self.device == self.installed_device: diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7a4b369e5..cc67db47c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -172,16 +172,6 @@ class PrefixForm(BootstrapMixin, CustomFieldForm): else: self.fields['vlan'].choices = [] - def clean_prefix(self): - prefix = self.cleaned_data['prefix'] - if prefix.version == 4 and prefix.prefixlen == 32: - raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 " - "addresses instead.") - elif prefix.version == 6 and prefix.prefixlen == 128: - raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 " - "addresses instead.") - return prefix - class PrefixFromCSVForm(forms.ModelForm): vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 88d83470e..e8f4238b8 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -139,16 +139,22 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): if self.pk: covering_aggregates = covering_aggregates.exclude(pk=self.pk) if covering_aggregates: - raise ValidationError("{} is already covered by an existing aggregate ({})" - .format(self.prefix, covering_aggregates[0])) + raise ValidationError({ + 'prefix': "Aggregates cannot overlap. {} is already covered by an existing aggregate ({}).".format( + self.prefix, covering_aggregates[0] + ) + }) # Ensure that the aggregate being added does not cover an existing aggregate covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix)) if self.pk: covered_aggregates = covered_aggregates.exclude(pk=self.pk) if covered_aggregates: - raise ValidationError("{} overlaps with an existing aggregate ({})" - .format(self.prefix, covered_aggregates[0])) + raise ValidationError({ + 'prefix': "Aggregates cannot overlap. {} covers an existing aggregate ({}).".format( + self.prefix, covered_aggregates[0] + ) + }) def save(self, *args, **kwargs): if self.prefix: @@ -268,14 +274,17 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): return reverse('ipam:prefix', args=[self.pk]) def clean(self): + # Disallow host masks if self.prefix: if self.prefix.version == 4 and self.prefix.prefixlen == 32: - raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses " - "instead.") + raise ValidationError({ + 'prefix': "Cannot create host addresses (/32) as prefixes. Create an IPv4 address instead." + }) elif self.prefix.version == 6 and self.prefix.prefixlen == 128: - raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses " - "instead.") + raise ValidationError({ + 'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead." + }) def save(self, *args, **kwargs): if self.prefix: @@ -369,13 +378,16 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\ .exclude(pk=self.pk) if duplicate_ips: - raise ValidationError("Duplicate IP address found in VRF {}: {}".format(self.vrf, - duplicate_ips.first())) + raise ValidationError({ + 'address': "Duplicate IP address found in VRF {}: {}".format(self.vrf, duplicate_ips.first()) + }) elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE: duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\ .exclude(pk=self.pk) if duplicate_ips: - raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first())) + raise ValidationError({ + 'address': "Duplicate IP address found in global table: {}".format(duplicate_ips.first()) + }) def save(self, *args, **kwargs): if self.address: @@ -478,7 +490,9 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): # Validate VLAN group if self.group and self.group.site != self.site: - raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site)) + raise ValidationError({ + 'group': "VLAN group must belong to the assigned site ({}).".format(self.site) + }) def to_csv(self): return ','.join([ diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index a163640b8..7eb9e816a 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -57,14 +57,14 @@ class SecretForm(forms.ModelForm, BootstrapMixin): fields = ['role', 'name', 'plaintext', 'plaintext2'] def clean(self): + if self.cleaned_data['plaintext']: validate_rsa_key(self.cleaned_data['private_key']) - def clean_plaintext2(self): - plaintext = self.cleaned_data['plaintext'] - plaintext2 = self.cleaned_data['plaintext2'] - if plaintext != plaintext2: - raise forms.ValidationError("The two given plaintext values do not match. Please check your input.") + if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']: + raise forms.ValidationError({ + 'plaintext2': "The two given plaintext values do not match. Please check your input." + }) class SecretFromCSVForm(forms.ModelForm): diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 80abfcbdf..930d6e032 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -81,24 +81,34 @@ class UserKey(CreatedUpdatedModel): def clean(self, *args, **kwargs): - # Validate the public key format and length. if self.public_key: + + # Validate the public key format try: pubkey = RSA.importKey(self.public_key) except ValueError: - raise ValidationError("Invalid RSA key format.") + raise ValidationError({ + 'public_key': "Invalid RSA key format." + }) except: raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're " "uploading a valid RSA public key in PEM format (no SSH/PGP).") - # key.size() returns 1 less than the key modulus - pubkey_length = pubkey.size() + 1 + + # Validate the public key length + pubkey_length = pubkey.size() + 1 # key.size() returns 1 less than the key modulus if pubkey_length < settings.SECRETS_MIN_PUBKEY_SIZE: - raise ValidationError("Insufficient key length. Keys must be at least {} bits long." - .format(settings.SECRETS_MIN_PUBKEY_SIZE)) + raise ValidationError({ + 'public_key': "Insufficient key length. Keys must be at least {} bits long.".format( + settings.SECRETS_MIN_PUBKEY_SIZE + ) + }) # We can't use keys bigger than our master_key_cipher field can hold if pubkey_length > 4096: - raise ValidationError("Public key size ({}) is too large. Maximum key size is 4096 bits." - .format(pubkey_length)) + raise ValidationError({ + 'public_key': "Public key size ({}) is too large. Maximum key size is 4096 bits.".format( + pubkey_length + ) + }) super(UserKey, self).clean()