mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
Misc cleanup, capitalization fixes
This commit is contained in:
parent
28119157e4
commit
9d2437c3b7
@ -34,7 +34,7 @@ class Circuit(PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
cid = models.CharField(
|
cid = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
verbose_name=_('Circuit ID'),
|
verbose_name=_('circuit ID'),
|
||||||
help_text=_('Unique circuit ID')
|
help_text=_('Unique circuit ID')
|
||||||
)
|
)
|
||||||
provider = models.ForeignKey(
|
provider = models.ForeignKey(
|
||||||
@ -70,17 +70,17 @@ class Circuit(PrimaryModel):
|
|||||||
install_date = models.DateField(
|
install_date = models.DateField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Installed')
|
verbose_name=_('installed')
|
||||||
)
|
)
|
||||||
termination_date = models.DateField(
|
termination_date = models.DateField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Terminates')
|
verbose_name=_('terminates')
|
||||||
)
|
)
|
||||||
commit_rate = models.PositiveIntegerField(
|
commit_rate = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Commit rate (Kbps)'),
|
verbose_name=_('commit rate (Kbps)'),
|
||||||
help_text=_("Committed rate")
|
help_text=_("Committed rate")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ class CircuitTermination(
|
|||||||
term_side = models.CharField(
|
term_side = models.CharField(
|
||||||
max_length=1,
|
max_length=1,
|
||||||
choices=CircuitTerminationSideChoices,
|
choices=CircuitTerminationSideChoices,
|
||||||
verbose_name=_('Termination')
|
verbose_name=_('termination')
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
site = models.ForeignKey(
|
||||||
to='dcim.Site',
|
to='dcim.Site',
|
||||||
@ -180,7 +180,7 @@ class CircuitTermination(
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
port_speed = models.PositiveIntegerField(
|
port_speed = models.PositiveIntegerField(
|
||||||
verbose_name=_('Port speed (Kbps)'),
|
verbose_name=_('port speed (Kbps)'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_('Physical circuit speed')
|
help_text=_('Physical circuit speed')
|
||||||
@ -188,19 +188,19 @@ class CircuitTermination(
|
|||||||
upstream_speed = models.PositiveIntegerField(
|
upstream_speed = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Upstream speed (Kbps)'),
|
verbose_name=_('upstream speed (Kbps)'),
|
||||||
help_text=_('Upstream speed, if different from port speed')
|
help_text=_('Upstream speed, if different from port speed')
|
||||||
)
|
)
|
||||||
xconnect_id = models.CharField(
|
xconnect_id = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Cross-connect ID'),
|
verbose_name=_('cross-connect ID'),
|
||||||
help_text=_('ID of the local cross-connect')
|
help_text=_('ID of the local cross-connect')
|
||||||
)
|
)
|
||||||
pp_info = models.CharField(
|
pp_info = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Patch panel/port(s)'),
|
verbose_name=_('patch panel/port(s)'),
|
||||||
help_text=_('Patch panel ID and port number(s)')
|
help_text=_('Patch panel ID and port number(s)')
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
@ -63,7 +63,7 @@ class ProviderAccount(PrimaryModel):
|
|||||||
)
|
)
|
||||||
account = models.CharField(
|
account = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
verbose_name=_('Account ID')
|
verbose_name=_('account ID')
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
@ -118,7 +118,7 @@ class ProviderNetwork(PrimaryModel):
|
|||||||
service_id = models.CharField(
|
service_id = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Service ID')
|
verbose_name=_('service ID')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
@ -54,19 +53,19 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
)
|
)
|
||||||
provider_account = tables.Column(
|
provider_account = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Account')
|
verbose_name='Account'
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
termination_a = tables.TemplateColumn(
|
termination_a = tables.TemplateColumn(
|
||||||
template_code=CIRCUITTERMINATION_LINK,
|
template_code=CIRCUITTERMINATION_LINK,
|
||||||
verbose_name=_('Side A')
|
verbose_name='Side A'
|
||||||
)
|
)
|
||||||
termination_z = tables.TemplateColumn(
|
termination_z = tables.TemplateColumn(
|
||||||
template_code=CIRCUITTERMINATION_LINK,
|
template_code=CIRCUITTERMINATION_LINK,
|
||||||
verbose_name=_('Side Z')
|
verbose_name='Side Z'
|
||||||
)
|
)
|
||||||
commit_rate = CommitRateColumn(
|
commit_rate = CommitRateColumn(
|
||||||
verbose_name=_('Commit Rate')
|
verbose_name='Commit Rate'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -266,7 +266,8 @@ class DataFile(models.Model):
|
|||||||
help_text=_("File path relative to the data source's root")
|
help_text=_("File path relative to the data source's root")
|
||||||
)
|
)
|
||||||
size = models.PositiveIntegerField(
|
size = models.PositiveIntegerField(
|
||||||
editable=False
|
editable=False,
|
||||||
|
verbose_name=_('size')
|
||||||
)
|
)
|
||||||
hash = models.CharField(
|
hash = models.CharField(
|
||||||
verbose_name=_('hash'),
|
verbose_name=_('hash'),
|
||||||
|
@ -93,7 +93,7 @@ class Job(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
job_id = models.UUIDField(
|
job_id = models.UUIDField(
|
||||||
verbose_name=_('job id'),
|
verbose_name=_('job ID'),
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
cable_end = models.CharField(
|
cable_end = models.CharField(
|
||||||
max_length=1,
|
max_length=1,
|
||||||
choices=CableEndChoices,
|
choices=CableEndChoices,
|
||||||
verbose_name=_('End')
|
verbose_name=_('end')
|
||||||
)
|
)
|
||||||
termination_type = models.ForeignKey(
|
termination_type = models.ForeignKey(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
|
@ -43,9 +43,9 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64,
|
max_length=64,
|
||||||
help_text=_("""
|
help_text=_(
|
||||||
{module} is accepted as a substitution for the module bay position when attached to a module type.
|
"{module} is accepted as a substitution for the module bay position when attached to a module type."
|
||||||
""")
|
)
|
||||||
)
|
)
|
||||||
_name = NaturalOrderingField(
|
_name = NaturalOrderingField(
|
||||||
target_field='name',
|
target_field='name',
|
||||||
@ -59,6 +59,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
|||||||
help_text=_('Physical label')
|
help_text=_('Physical label')
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
@ -378,7 +379,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
mgmt_only = models.BooleanField(
|
mgmt_only = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Management only')
|
verbose_name=_('management only')
|
||||||
)
|
)
|
||||||
bridge = models.ForeignKey(
|
bridge = models.ForeignKey(
|
||||||
to='self',
|
to='self',
|
||||||
@ -386,7 +387,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
related_name='bridge_interfaces',
|
related_name='bridge_interfaces',
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Bridge interface')
|
verbose_name=_('bridge interface')
|
||||||
)
|
)
|
||||||
poe_mode = models.CharField(
|
poe_mode = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -404,7 +405,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
choices=WirelessRoleChoices,
|
choices=WirelessRoleChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Wireless role'
|
verbose_name=_('wireless role')
|
||||||
)
|
)
|
||||||
|
|
||||||
component_model = Interface
|
component_model = Interface
|
||||||
@ -703,7 +704,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
part_id = models.CharField(
|
part_id = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_('Part ID'),
|
verbose_name=_('part ID'),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Manufacturer-assigned part identifier')
|
help_text=_('Manufacturer-assigned part identifier')
|
||||||
)
|
)
|
||||||
|
@ -200,7 +200,9 @@ class CabledObjectModel(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_object(self):
|
def parent_object(self):
|
||||||
raise NotImplementedError(_("{class_name} models must declare a parent_object property").format(class_name=self.__class__.__name__))
|
raise NotImplementedError(
|
||||||
|
_("{class_name} models must declare a parent_object property").format(class_name=self.__class__.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def opposite_cable_end(self):
|
def opposite_cable_end(self):
|
||||||
@ -366,7 +368,9 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
|
|||||||
if self.maximum_draw is not None and self.allocated_draw is not None:
|
if self.maximum_draw is not None and self.allocated_draw is not None:
|
||||||
if self.allocated_draw > self.maximum_draw:
|
if self.allocated_draw > self.maximum_draw:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'allocated_draw': _("Allocated draw cannot exceed the maximum draw ({maximum_draw}W).").format(maximum_draw=self.maximum_draw)
|
'allocated_draw': _(
|
||||||
|
"Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
|
||||||
|
).format(maximum_draw=self.maximum_draw)
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_downstream_powerports(self, leg=None):
|
def get_downstream_powerports(self, leg=None):
|
||||||
@ -477,7 +481,9 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
|||||||
|
|
||||||
# Validate power port assignment
|
# Validate power port assignment
|
||||||
if self.power_port and self.power_port.device != self.device:
|
if self.power_port and self.power_port.device != self.device:
|
||||||
raise ValidationError(_("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port))
|
raise ValidationError(
|
||||||
|
_("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -495,7 +501,7 @@ class BaseInterface(models.Model):
|
|||||||
mac_address = MACAddressField(
|
mac_address = MACAddressField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('MAC Address')
|
verbose_name=_('MAC address')
|
||||||
)
|
)
|
||||||
mtu = models.PositiveIntegerField(
|
mtu = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -575,7 +581,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
related_name='member_interfaces',
|
related_name='member_interfaces',
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Parent LAG')
|
verbose_name=_('parent LAG')
|
||||||
)
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
verbose_name=_('type'),
|
verbose_name=_('type'),
|
||||||
@ -584,13 +590,13 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
)
|
)
|
||||||
mgmt_only = models.BooleanField(
|
mgmt_only = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Management only'),
|
verbose_name=_('management only'),
|
||||||
help_text=_('This interface is used only for out-of-band management')
|
help_text=_('This interface is used only for out-of-band management')
|
||||||
)
|
)
|
||||||
speed = models.PositiveIntegerField(
|
speed = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Speed (Kbps)')
|
verbose_name=_('speed (Kbps)')
|
||||||
)
|
)
|
||||||
duplex = models.CharField(
|
duplex = models.CharField(
|
||||||
verbose_name=_('duplex'),
|
verbose_name=_('duplex'),
|
||||||
@ -609,20 +615,20 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
choices=WirelessRoleChoices,
|
choices=WirelessRoleChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Wireless role')
|
verbose_name=_('wireless role')
|
||||||
)
|
)
|
||||||
rf_channel = models.CharField(
|
rf_channel = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=WirelessChannelChoices,
|
choices=WirelessChannelChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Wireless channel')
|
verbose_name=_('wireless channel')
|
||||||
)
|
)
|
||||||
rf_channel_frequency = models.DecimalField(
|
rf_channel_frequency = models.DecimalField(
|
||||||
max_digits=7,
|
max_digits=7,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Channel frequency (MHz)'),
|
verbose_name=_('channel frequency (MHz)'),
|
||||||
help_text=_("Populated by selected channel (if set)")
|
help_text=_("Populated by selected channel (if set)")
|
||||||
)
|
)
|
||||||
rf_channel_width = models.DecimalField(
|
rf_channel_width = models.DecimalField(
|
||||||
@ -630,14 +636,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
decimal_places=3,
|
decimal_places=3,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=('Channel width (MHz)'),
|
verbose_name=('channel width (MHz)'),
|
||||||
help_text=_("Populated by selected channel (if set)")
|
help_text=_("Populated by selected channel (if set)")
|
||||||
)
|
)
|
||||||
tx_power = models.PositiveSmallIntegerField(
|
tx_power = models.PositiveSmallIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=(MaxValueValidator(127),),
|
validators=(MaxValueValidator(127),),
|
||||||
verbose_name=_('Transmit power (dBm)')
|
verbose_name=_('transmit power (dBm)')
|
||||||
)
|
)
|
||||||
poe_mode = models.CharField(
|
poe_mode = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -662,7 +668,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
to='wireless.WirelessLAN',
|
to='wireless.WirelessLAN',
|
||||||
related_name='interfaces',
|
related_name='interfaces',
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Wireless LANs')
|
verbose_name=_('wireless LANs')
|
||||||
)
|
)
|
||||||
untagged_vlan = models.ForeignKey(
|
untagged_vlan = models.ForeignKey(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
@ -670,13 +676,13 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
related_name='interfaces_as_untagged',
|
related_name='interfaces_as_untagged',
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Untagged VLAN')
|
verbose_name=_('untagged VLAN')
|
||||||
)
|
)
|
||||||
tagged_vlans = models.ManyToManyField(
|
tagged_vlans = models.ManyToManyField(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
related_name='interfaces_as_tagged',
|
related_name='interfaces_as_tagged',
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Tagged VLANs')
|
verbose_name=_('tagged VLANs')
|
||||||
)
|
)
|
||||||
vrf = models.ForeignKey(
|
vrf = models.ForeignKey(
|
||||||
to='ipam.VRF',
|
to='ipam.VRF',
|
||||||
@ -722,13 +728,17 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
# Virtual Interfaces cannot have a Cable attached
|
# Virtual Interfaces cannot have a Cable attached
|
||||||
if self.is_virtual and self.cable:
|
if self.is_virtual and self.cable:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'type': _("{display_type} interfaces cannot have a cable attached.").format(display_type=self.get_type_display())
|
'type': _("{display_type} interfaces cannot have a cable attached.").format(
|
||||||
|
display_type=self.get_type_display()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Virtual Interfaces cannot be marked as connected
|
# Virtual Interfaces cannot be marked as connected
|
||||||
if self.is_virtual and self.mark_connected:
|
if self.is_virtual and self.mark_connected:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'mark_connected': _("{display_type} interfaces cannot be marked as connected.".format(display_type=self.get_type_display()))
|
'mark_connected': _("{display_type} interfaces cannot be marked as connected.".format(
|
||||||
|
display_type=self.get_type_display())
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Parent validation
|
# Parent validation
|
||||||
@ -745,15 +755,20 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
if self.parent and self.parent.device != self.device:
|
if self.parent and self.parent.device != self.device:
|
||||||
if self.device.virtual_chassis is None:
|
if self.device.virtual_chassis is None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'parent': _("The selected parent interface ({selected_parent}) belongs to a different device ({parent_device})").format(
|
'parent': _(
|
||||||
selected_parent=self.parent, parent_device=self.parent.device)
|
"The selected parent interface ({interface}) belongs to a different device ({device})"
|
||||||
|
).format(interface=self.parent, device=self.parent.device)
|
||||||
})
|
})
|
||||||
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
|
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'parent': _("""
|
'parent': _(
|
||||||
The selected parent interface ({parent}) belongs to {parent_device}, which
|
"The selected parent interface ({interface}) belongs to {device}, which is not part of "
|
||||||
is not part of virtual chassis {virtual_chassis}.
|
"virtual chassis {virtual_chassis}."
|
||||||
""").format(parent=self.parent, parent_device=self.parent_device, virtual_chassis=self.device.virtual_chassis)
|
).format(
|
||||||
|
interface=self.parent,
|
||||||
|
device=self.parent_device,
|
||||||
|
virtual_chassis=self.device.virtual_chassis
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Bridge validation
|
# Bridge validation
|
||||||
@ -772,10 +787,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
})
|
})
|
||||||
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
|
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'bridge': _("""
|
'bridge': _(
|
||||||
The selected bridge interface ({bridge}) belongs to {device}, which "
|
"The selected bridge interface ({interface}) belongs to {device}, which is not part of virtual "
|
||||||
is not part of virtual chassis {virtual_chassis}.
|
"chassis {virtual_chassis}."
|
||||||
""").format(bridge=self.bridge, device=self.bridge.device, virtual_chassis=self.device.virtual_chassis)
|
).format(
|
||||||
|
interface=self.bridge, device=self.bridge.device, virtual_chassis=self.device.virtual_chassis
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# LAG validation
|
# LAG validation
|
||||||
@ -792,14 +809,17 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
if self.lag and self.lag.device != self.device:
|
if self.lag and self.lag.device != self.device:
|
||||||
if self.device.virtual_chassis is None:
|
if self.device.virtual_chassis is None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'lag': _("The selected LAG interface ({lag}) belongs to a different device ({device}).").format(lag=self.lag, device=self.lag.device)
|
'lag': _(
|
||||||
|
"The selected LAG interface ({lag}) belongs to a different device ({device})."
|
||||||
|
).format(lag=self.lag, device=self.lag.device)
|
||||||
})
|
})
|
||||||
elif self.lag.device.virtual_chassis != self.device.virtual_chassis:
|
elif self.lag.device.virtual_chassis != self.device.virtual_chassis:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'lag': _("""
|
'lag': _(
|
||||||
The selected LAG interface ({lag}) belongs to {device}, which is not part
|
"The selected LAG interface ({lag}) belongs to {device}, which is not part of virtual chassis "
|
||||||
of virtual chassis {virtual_chassis}.
|
"{virtual_chassis}.".format(
|
||||||
""".format(lag=self.lag, device=self.lag.device, virtual_chassis=self.device.virtual_chassis))
|
lag=self.lag, device=self.lag.device, virtual_chassis=self.device.virtual_chassis)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# PoE validation
|
# PoE validation
|
||||||
@ -935,7 +955,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
related_name='frontports'
|
related_name='frontports'
|
||||||
)
|
)
|
||||||
rear_port_position = models.PositiveSmallIntegerField(
|
rear_port_position = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('rear_port_position'),
|
verbose_name=_('rear port position'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
@ -969,16 +989,22 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
# Validate rear port assignment
|
# Validate rear port assignment
|
||||||
if self.rear_port.device != self.device:
|
if self.rear_port.device != self.device:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device").format(rear_port=self.rear_port)
|
"rear_port": _(
|
||||||
|
"Rear port ({rear_port}) must belong to the same device"
|
||||||
|
).format(rear_port=self.rear_port)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate rear port position assignment
|
# Validate rear port position assignment
|
||||||
if self.rear_port_position > self.rear_port.positions:
|
if self.rear_port_position > self.rear_port.positions:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"rear_port_position": _("""
|
"rear_port_position": _(
|
||||||
Invalid rear port position ({rear_port_position}): Rear port
|
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||||
{name} has only {positions} positions
|
"positions."
|
||||||
""").format(rear_port_position=self.rear_port_position, name=self.rear_port.name, positions=self.rear_port.positions)
|
).format(
|
||||||
|
rear_port_position=self.rear_port_position,
|
||||||
|
name=self.rear_port.name,
|
||||||
|
positions=self.rear_port.positions
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1066,8 +1092,8 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
|||||||
|
|
||||||
# Validate that the parent Device can have DeviceBays
|
# Validate that the parent Device can have DeviceBays
|
||||||
if not self.device.device_type.is_parent_device:
|
if not self.device.device_type.is_parent_device:
|
||||||
raise ValidationError(_("This type of device ({}) does not support device bays.").format(
|
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
|
||||||
self.device.device_type
|
device_type=self.device.device_type
|
||||||
))
|
))
|
||||||
|
|
||||||
# Cannot install a device into itself, obviously
|
# Cannot install a device into itself, obviously
|
||||||
@ -1079,9 +1105,9 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
|||||||
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
|
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
|
||||||
if current_bay and current_bay != self:
|
if current_bay and current_bay != self:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'installed_device': _("Cannot install the specified device; device is already installed in {}").format(
|
'installed_device': _(
|
||||||
current_bay
|
"Cannot install the specified device; device is already installed in {bay}."
|
||||||
)
|
).format(bay=current_bay)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1148,13 +1174,13 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
|||||||
)
|
)
|
||||||
part_id = models.CharField(
|
part_id = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_('Part ID'),
|
verbose_name=_('part ID'),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Manufacturer-assigned part identifier')
|
help_text=_('Manufacturer-assigned part identifier')
|
||||||
)
|
)
|
||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_('Serial number'),
|
verbose_name=_('serial number'),
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
asset_tag = models.CharField(
|
asset_tag = models.CharField(
|
||||||
@ -1162,7 +1188,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
|||||||
unique=True,
|
unique=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Asset tag'),
|
verbose_name=_('asset tag'),
|
||||||
help_text=_('A unique tag used to identify this item')
|
help_text=_('A unique tag used to identify this item')
|
||||||
)
|
)
|
||||||
discovered = models.BooleanField(
|
discovered = models.BooleanField(
|
||||||
|
@ -91,7 +91,7 @@ class DeviceType(PrimaryModel, WeightMixin):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Default platform')
|
verbose_name=_('default platform')
|
||||||
)
|
)
|
||||||
part_number = models.CharField(
|
part_number = models.CharField(
|
||||||
verbose_name=_('part number'),
|
verbose_name=_('part number'),
|
||||||
@ -103,18 +103,18 @@ class DeviceType(PrimaryModel, WeightMixin):
|
|||||||
max_digits=4,
|
max_digits=4,
|
||||||
decimal_places=1,
|
decimal_places=1,
|
||||||
default=1.0,
|
default=1.0,
|
||||||
verbose_name=_('Height (U)')
|
verbose_name=_('height (U)')
|
||||||
)
|
)
|
||||||
is_full_depth = models.BooleanField(
|
is_full_depth = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_('Is full depth'),
|
verbose_name=_('is full depth'),
|
||||||
help_text=_('Device consumes both front and rear rack faces')
|
help_text=_('Device consumes both front and rear rack faces')
|
||||||
)
|
)
|
||||||
subdevice_role = models.CharField(
|
subdevice_role = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=SubdeviceRoleChoices,
|
choices=SubdeviceRoleChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Parent/child status'),
|
verbose_name=_('parent/child status'),
|
||||||
help_text=_('Parent devices house child devices in device bays. Leave blank '
|
help_text=_('Parent devices house child devices in device bays. Leave blank '
|
||||||
'if this device type is neither a parent nor a child.')
|
'if this device type is neither a parent nor a child.')
|
||||||
)
|
)
|
||||||
@ -180,7 +180,8 @@ class DeviceType(PrimaryModel, WeightMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
|
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
|
||||||
|
'weight_unit',
|
||||||
)
|
)
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
'dcim.Manufacturer',
|
'dcim.Manufacturer',
|
||||||
@ -310,10 +311,10 @@ class DeviceType(PrimaryModel, WeightMixin):
|
|||||||
if racked_instance_count:
|
if racked_instance_count:
|
||||||
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
|
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'u_height': mark_safe(
|
'u_height': mark_safe(_(
|
||||||
_('Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
|
'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
|
||||||
'mounted within racks.').format(url=url, racked_instance_count=racked_instance_count)
|
'mounted within racks.'
|
||||||
)
|
).format(url=url, racked_instance_count=racked_instance_count))
|
||||||
})
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -465,7 +466,7 @@ class DeviceRole(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
vm_role = models.BooleanField(
|
vm_role = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_('VM Role'),
|
verbose_name=_('VM role'),
|
||||||
help_text=_('Virtual machines may be assigned to this role')
|
help_text=_('Virtual machines may be assigned to this role')
|
||||||
)
|
)
|
||||||
config_template = models.ForeignKey(
|
config_template = models.ForeignKey(
|
||||||
@ -571,7 +572,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Serial number'),
|
verbose_name=_('serial number'),
|
||||||
help_text=_("Chassis serial number, assigned by the manufacturer")
|
help_text=_("Chassis serial number, assigned by the manufacturer")
|
||||||
)
|
)
|
||||||
asset_tag = models.CharField(
|
asset_tag = models.CharField(
|
||||||
@ -579,7 +580,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_('Asset tag'),
|
verbose_name=_('asset tag'),
|
||||||
help_text=_('A unique tag used to identify this device')
|
help_text=_('A unique tag used to identify this device')
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
site = models.ForeignKey(
|
||||||
@ -607,14 +608,14 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
|
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
|
||||||
verbose_name=_('Position (U)'),
|
verbose_name=_('position (U)'),
|
||||||
help_text=_('The lowest-numbered unit occupied by the device')
|
help_text=_('The lowest-numbered unit occupied by the device')
|
||||||
)
|
)
|
||||||
face = models.CharField(
|
face = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
choices=DeviceFaceChoices,
|
choices=DeviceFaceChoices,
|
||||||
verbose_name=_('Rack face')
|
verbose_name=_('rack face')
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
verbose_name=_('status'),
|
verbose_name=_('status'),
|
||||||
@ -634,7 +635,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Primary IPv4')
|
verbose_name=_('primary IPv4')
|
||||||
)
|
)
|
||||||
primary_ip6 = models.OneToOneField(
|
primary_ip6 = models.OneToOneField(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
@ -642,7 +643,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Primary IPv6')
|
verbose_name=_('primary IPv6')
|
||||||
)
|
)
|
||||||
oob_ip = models.OneToOneField(
|
oob_ip = models.OneToOneField(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
@ -650,7 +651,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='Out-of-band IP'
|
verbose_name=_('out-of-band IP')
|
||||||
)
|
)
|
||||||
cluster = models.ForeignKey(
|
cluster = models.ForeignKey(
|
||||||
to='virtualization.Cluster',
|
to='virtualization.Cluster',
|
||||||
@ -667,14 +668,14 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
vc_position = models.PositiveSmallIntegerField(
|
vc_position = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('vc position'),
|
verbose_name=_('VC position'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MaxValueValidator(255)],
|
validators=[MaxValueValidator(255)],
|
||||||
help_text=_('Virtual chassis position')
|
help_text=_('Virtual chassis position')
|
||||||
)
|
)
|
||||||
vc_priority = models.PositiveSmallIntegerField(
|
vc_priority = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('vc priority'),
|
verbose_name=_('VC priority'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MaxValueValidator(255)],
|
validators=[MaxValueValidator(255)],
|
||||||
@ -817,11 +818,15 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
})
|
})
|
||||||
if self.location and self.site != self.location.site:
|
if self.location and self.site != self.location.site:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'location': _("Location {location} does not belong to site {site}.").format(location=self.location, site=self.site),
|
'location': _(
|
||||||
|
"Location {location} does not belong to site {site}."
|
||||||
|
).format(location=self.location, site=self.site)
|
||||||
})
|
})
|
||||||
if self.rack and self.location and self.rack.location != self.location:
|
if self.rack and self.location and self.rack.location != self.location:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'rack': _("Rack {rack} does not belong to location {location}.").format(rack=self.rack, location=self.location),
|
'rack': _(
|
||||||
|
"Rack {rack} does not belong to location {location}."
|
||||||
|
).format(rack=self.rack, location=self.location)
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.rack is None:
|
if self.rack is None:
|
||||||
@ -848,7 +853,9 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
if hasattr(self, 'device_type'):
|
if hasattr(self, 'device_type'):
|
||||||
if self.position and self.device_type.u_height == 0:
|
if self.position and self.device_type.u_height == 0:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'position': _("A U0 device type ({device_type}) cannot be assigned to a rack position.").format(device_type=self.device_type)
|
'position': _(
|
||||||
|
"A U0 device type ({device_type}) cannot be assigned to a rack position."
|
||||||
|
).format(device_type=self.device_type)
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.rack:
|
if self.rack:
|
||||||
@ -857,13 +864,17 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
# Child devices cannot be assigned to a rack face/unit
|
# Child devices cannot be assigned to a rack face/unit
|
||||||
if self.device_type.is_child_device and self.face:
|
if self.device_type.is_child_device and self.face:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'face': _("Child device types cannot be assigned to a rack face. This is an attribute of the "
|
'face': _(
|
||||||
"parent device.")
|
"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:
|
if self.device_type.is_child_device and self.position:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'position': _("Child device types cannot be assigned to a rack position. This is an attribute of "
|
'position': _(
|
||||||
"the parent device.")
|
"Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||||
|
"parent device."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate rack space
|
# Validate rack space
|
||||||
@ -874,9 +885,12 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
)
|
)
|
||||||
if self.position and self.position not in available_units:
|
if self.position and self.position not in available_units:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'position': _("U{position} is already occupied or does not have sufficient space to "
|
'position': _(
|
||||||
"accommodate this device type: {device_type} ({u_height}U)").format(
|
"U{position} is already occupied or does not have sufficient space to accommodate this "
|
||||||
position=self.position, device_type=self.device_type, u_height=self.device_type.u_height)
|
"device type: {device_type} ({u_height}U)"
|
||||||
|
).format(
|
||||||
|
position=self.position, device_type=self.device_type, u_height=self.device_type.u_height
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
except DeviceType.DoesNotExist:
|
except DeviceType.DoesNotExist:
|
||||||
@ -895,7 +909,9 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'primary_ip4': _("The specified IP address ({primary_ip4}) is not assigned to this device.").format(primary_ip4=self.primary_ip4)
|
'primary_ip4': _(
|
||||||
|
"The specified IP address ({primary_ip4}) is not assigned to this device."
|
||||||
|
).format(primary_ip4=self.primary_ip4)
|
||||||
})
|
})
|
||||||
if self.primary_ip6:
|
if self.primary_ip6:
|
||||||
if self.primary_ip6.family != 6:
|
if self.primary_ip6.family != 6:
|
||||||
@ -908,7 +924,9 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'primary_ip6': _("The specified IP address ({primary_ip6}) is not assigned to this device.").format(primary_ip6=self.primary_ip6)
|
'primary_ip6': _(
|
||||||
|
"The specified IP address ({primary_ip6}) is not assigned to this device."
|
||||||
|
).format(primary_ip6=self.primary_ip6)
|
||||||
})
|
})
|
||||||
if self.oob_ip:
|
if self.oob_ip:
|
||||||
if self.oob_ip.assigned_object in vc_interfaces:
|
if self.oob_ip.assigned_object in vc_interfaces:
|
||||||
@ -924,9 +942,13 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
|||||||
if hasattr(self, 'device_type') and self.platform:
|
if hasattr(self, 'device_type') and self.platform:
|
||||||
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
|
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'platform': _("The assigned platform is limited to {platform_manufacturer} device types, but "
|
'platform': _(
|
||||||
"this device's type belongs to {device_type_manufacturer}.").format(
|
"The assigned platform is limited to {platform_manufacturer} device types, but this device's "
|
||||||
platform_manufacturer=self.platform.manufacturer, device_type_manufacturer=self.device_type.manufacturer)
|
"type belongs to {device_type_manufacturer}."
|
||||||
|
).format(
|
||||||
|
platform_manufacturer=self.platform.manufacturer,
|
||||||
|
device_type_manufacturer=self.device_type.manufacturer
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# A Device can only be assigned to a Cluster in the same Site (or no Site)
|
# A Device can only be assigned to a Cluster in the same Site (or no Site)
|
||||||
@ -1131,14 +1153,14 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Serial number')
|
verbose_name=_('serial number')
|
||||||
)
|
)
|
||||||
asset_tag = models.CharField(
|
asset_tag = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_('Asset tag'),
|
verbose_name=_('asset tag'),
|
||||||
help_text=_('A unique tag used to identify this device')
|
help_text=_('A unique tag used to identify this device')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1161,7 +1183,9 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
|
|
||||||
if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
|
if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("Module must be installed within a module bay belonging to the assigned device ({device}).").format(device=self.device)
|
_("Module must be installed within a module bay belonging to the assigned device ({device}).").format(
|
||||||
|
device=self.device
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -1291,7 +1315,9 @@ class VirtualChassis(PrimaryModel):
|
|||||||
# VirtualChassis.)
|
# VirtualChassis.)
|
||||||
if self.pk and self.master and self.master not in self.members.all():
|
if self.pk and self.master and self.master not in self.members.all():
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(master=self.master)
|
'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(
|
||||||
|
master=self.master
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
@ -1304,10 +1330,10 @@ class VirtualChassis(PrimaryModel):
|
|||||||
lag__device=F('device')
|
lag__device=F('device')
|
||||||
)
|
)
|
||||||
if interfaces:
|
if interfaces:
|
||||||
raise ProtectedError(
|
raise ProtectedError(_(
|
||||||
_("Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG interfaces").format(
|
"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG "
|
||||||
self=self, interfaces=InterfaceSpeedChoices),
|
"interfaces."
|
||||||
)
|
).format(self=self, interfaces=InterfaceSpeedChoices))
|
||||||
|
|
||||||
return super().delete(*args, **kwargs)
|
return super().delete(*args, **kwargs)
|
||||||
|
|
||||||
@ -1341,7 +1367,7 @@ class VirtualDeviceContext(PrimaryModel):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Primary IPv4')
|
verbose_name=_('primary IPv4')
|
||||||
)
|
)
|
||||||
primary_ip6 = models.OneToOneField(
|
primary_ip6 = models.OneToOneField(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
@ -1349,7 +1375,7 @@ class VirtualDeviceContext(PrimaryModel):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Primary IPv6')
|
verbose_name=_('primary IPv6')
|
||||||
)
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
@ -1359,7 +1385,7 @@ class VirtualDeviceContext(PrimaryModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
verbose_name=_('comment'),
|
verbose_name=_('comments'),
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1405,7 +1431,9 @@ class VirtualDeviceContext(PrimaryModel):
|
|||||||
continue
|
continue
|
||||||
if primary_ip.family != family:
|
if primary_ip.family != family:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
f'primary_ip{family}': _("{primary_ip} is not an IPv{family} address.").format(family=family, primary_ip=primary_ip)
|
f'primary_ip{family}': _(
|
||||||
|
"{primary_ip} is not an IPv{family} address."
|
||||||
|
).format(family=family, primary_ip=primary_ip)
|
||||||
})
|
})
|
||||||
device_interfaces = self.device.vc_interfaces(if_master=False)
|
device_interfaces = self.device.vc_interfaces(if_master=False)
|
||||||
if primary_ip.assigned_object not in device_interfaces:
|
if primary_ip.assigned_object not in device_interfaces:
|
||||||
|
@ -14,7 +14,7 @@ class WeightMixin(models.Model):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
weight_unit = models.CharField(
|
weight_unit = models.CharField(
|
||||||
verbose_name=_('weight_unit'),
|
verbose_name=_('weight unit'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=WeightUnitChoices,
|
choices=WeightUnitChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -65,7 +65,7 @@ class Rack(PrimaryModel, WeightMixin):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Facility ID'),
|
verbose_name=_('facility ID'),
|
||||||
help_text=_("Locally-assigned identifier")
|
help_text=_("Locally-assigned identifier")
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
site = models.ForeignKey(
|
||||||
@ -104,42 +104,42 @@ class Rack(PrimaryModel, WeightMixin):
|
|||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Serial number')
|
verbose_name=_('serial number')
|
||||||
)
|
)
|
||||||
asset_tag = models.CharField(
|
asset_tag = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_('Asset tag'),
|
verbose_name=_('asset tag'),
|
||||||
help_text=_('A unique tag used to identify this rack')
|
help_text=_('A unique tag used to identify this rack')
|
||||||
)
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
choices=RackTypeChoices,
|
choices=RackTypeChoices,
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Type')
|
verbose_name=_('type')
|
||||||
)
|
)
|
||||||
width = models.PositiveSmallIntegerField(
|
width = models.PositiveSmallIntegerField(
|
||||||
choices=RackWidthChoices,
|
choices=RackWidthChoices,
|
||||||
default=RackWidthChoices.WIDTH_19IN,
|
default=RackWidthChoices.WIDTH_19IN,
|
||||||
verbose_name=_('Width'),
|
verbose_name=_('width'),
|
||||||
help_text=_('Rail-to-rail width')
|
help_text=_('Rail-to-rail width')
|
||||||
)
|
)
|
||||||
u_height = models.PositiveSmallIntegerField(
|
u_height = models.PositiveSmallIntegerField(
|
||||||
default=RACK_U_HEIGHT_DEFAULT,
|
default=RACK_U_HEIGHT_DEFAULT,
|
||||||
verbose_name=_('Height (U)'),
|
verbose_name=_('height (U)'),
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
|
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
|
||||||
help_text=_('Height in rack units')
|
help_text=_('Height in rack units')
|
||||||
)
|
)
|
||||||
starting_unit = models.PositiveSmallIntegerField(
|
starting_unit = models.PositiveSmallIntegerField(
|
||||||
default=RACK_STARTING_UNIT_DEFAULT,
|
default=RACK_STARTING_UNIT_DEFAULT,
|
||||||
verbose_name=_('Starting unit'),
|
verbose_name=_('starting unit'),
|
||||||
help_text=_('Starting unit for rack')
|
help_text=_('Starting unit for rack')
|
||||||
)
|
)
|
||||||
desc_units = models.BooleanField(
|
desc_units = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Descending units'),
|
verbose_name=_('descending units'),
|
||||||
help_text=_('Units are numbered top-to-bottom')
|
help_text=_('Units are numbered top-to-bottom')
|
||||||
)
|
)
|
||||||
outer_width = models.PositiveSmallIntegerField(
|
outer_width = models.PositiveSmallIntegerField(
|
||||||
|
@ -181,6 +181,7 @@ class Site(PrimaryModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
facility = models.CharField(
|
facility = models.CharField(
|
||||||
|
verbose_name=_('facility'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Local facility ID or description')
|
help_text=_('Local facility ID or description')
|
||||||
@ -337,4 +338,6 @@ class Location(NestedGroupModel):
|
|||||||
|
|
||||||
# Parent Location (if any) must belong to the same Site
|
# Parent Location (if any) must belong to the same Site
|
||||||
if self.parent and self.parent.site != self.site:
|
if self.parent and self.parent.site != self.site:
|
||||||
raise ValidationError(_("Parent location ({parent}) must belong to the same site ({site})").format(parent=self.parent, site=self.site))
|
raise ValidationError(_(
|
||||||
|
"Parent location ({parent}) must belong to the same site ({site})."
|
||||||
|
).format(parent=self.parent, site=self.site))
|
||||||
|
@ -77,13 +77,13 @@ class ObjectChange(models.Model):
|
|||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
prechange_data = models.JSONField(
|
prechange_data = models.JSONField(
|
||||||
verbose_name=_('prechange data'),
|
verbose_name=_('pre-change data'),
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
postchange_data = models.JSONField(
|
postchange_data = models.JSONField(
|
||||||
verbose_name=_('postchange data'),
|
verbose_name=_('post-change data'),
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
|
@ -221,7 +221,7 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
|
|||||||
help_text=_('Jinja2 template code.')
|
help_text=_('Jinja2 template code.')
|
||||||
)
|
)
|
||||||
environment_params = models.JSONField(
|
environment_params = models.JSONField(
|
||||||
verbose_name=_('environment params'),
|
verbose_name=_('environment parameters'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=dict,
|
default=dict,
|
||||||
|
@ -100,8 +100,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
verbose_name=_('label'),
|
verbose_name=_('label'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Name of the field as displayed to users (if not provided, '
|
help_text=_(
|
||||||
'the field\'s name will be used)')
|
"Name of the field as displayed to users (if not provided, 'the field's name will be used)"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
group_name = models.CharField(
|
group_name = models.CharField(
|
||||||
verbose_name=_('group name'),
|
verbose_name=_('group name'),
|
||||||
@ -117,52 +118,53 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
required = models.BooleanField(
|
required = models.BooleanField(
|
||||||
verbose_name=_('required'),
|
verbose_name=_('required'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_('If true, this field is required when creating new objects '
|
help_text=_("If true, this field is required when creating new objects or editing an existing object.")
|
||||||
'or editing an existing object.')
|
|
||||||
)
|
)
|
||||||
search_weight = models.PositiveSmallIntegerField(
|
search_weight = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('search weight'),
|
verbose_name=_('search weight'),
|
||||||
default=1000,
|
default=1000,
|
||||||
help_text=_('Weighting for search. Lower values are considered more important. '
|
help_text=_(
|
||||||
'Fields with a search weight of zero will be ignored.')
|
"Weighting for search. Lower values are considered more important. Fields with a search weight of zero "
|
||||||
|
"will be ignored."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
filter_logic = models.CharField(
|
filter_logic = models.CharField(
|
||||||
verbose_name=_('filter logic'),
|
verbose_name=_('filter logic'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CustomFieldFilterLogicChoices,
|
choices=CustomFieldFilterLogicChoices,
|
||||||
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
||||||
help_text=_('Loose matches any instance of a given string; exact '
|
help_text=_("Loose matches any instance of a given string; exact matches the entire field.")
|
||||||
'matches the entire field.')
|
|
||||||
)
|
)
|
||||||
default = models.JSONField(
|
default = models.JSONField(
|
||||||
verbose_name=_('default'),
|
verbose_name=_('default'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_('Default value for the field (must be a JSON value). Encapsulate '
|
help_text=_(
|
||||||
'strings with double quotes (e.g. "Foo").')
|
'Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo").'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
verbose_name=_('Display weight'),
|
verbose_name=_('display weight'),
|
||||||
help_text=_('Fields with higher weights appear lower in a form.')
|
help_text=_('Fields with higher weights appear lower in a form.')
|
||||||
)
|
)
|
||||||
validation_minimum = models.IntegerField(
|
validation_minimum = models.IntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Minimum value'),
|
verbose_name=_('minimum value'),
|
||||||
help_text=_('Minimum allowed value (for numeric fields)')
|
help_text=_('Minimum allowed value (for numeric fields)')
|
||||||
)
|
)
|
||||||
validation_maximum = models.IntegerField(
|
validation_maximum = models.IntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Maximum value'),
|
verbose_name=_('maximum value'),
|
||||||
help_text=_('Maximum allowed value (for numeric fields)')
|
help_text=_('Maximum allowed value (for numeric fields)')
|
||||||
)
|
)
|
||||||
validation_regex = models.CharField(
|
validation_regex = models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[validate_regex],
|
validators=[validate_regex],
|
||||||
max_length=500,
|
max_length=500,
|
||||||
verbose_name=_('Validation regex'),
|
verbose_name=_('validation regex'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For '
|
'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For '
|
||||||
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
||||||
@ -185,7 +187,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
is_cloneable = models.BooleanField(
|
is_cloneable = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Cloneable'),
|
verbose_name=_('is cloneable'),
|
||||||
help_text=_('Replicate this value when cloning objects')
|
help_text=_('Replicate this value when cloning objects')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -275,7 +277,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
self.validate(default_value)
|
self.validate(default_value)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'default': _('Invalid default value "{default}": {message}').format(default=self.default, message=self.message)
|
'default': _(
|
||||||
|
'Invalid default value "{default}": {message}'
|
||||||
|
).format(default=self.default, message=self.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Minimum/maximum values can be set only for numeric fields
|
# Minimum/maximum values can be set only for numeric fields
|
||||||
@ -313,7 +317,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
# A selection field's default (if any) must be present in its available choices
|
# A selection field's default (if any) must be present in its available choices
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
|
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'default': _("The specified default value ({default}) is not listed as an available choice.").format(default=self.default)
|
'default': _(
|
||||||
|
"The specified default value ({default}) is not listed as an available choice."
|
||||||
|
).format(default=self.default)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Object fields must define an object_type; other fields must not
|
# Object fields must define an object_type; other fields must not
|
||||||
@ -324,7 +330,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
})
|
})
|
||||||
elif self.object_type:
|
elif self.object_type:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'object_type': _("{type_display} fields may not define an object type.".format(type_display=self.get_type_display()))
|
'object_type': _(
|
||||||
|
"{type_display} fields may not define an object type.")
|
||||||
|
.format(type_display=self.get_type_display())
|
||||||
})
|
})
|
||||||
|
|
||||||
def serialize(self, value):
|
def serialize(self, value):
|
||||||
@ -473,7 +481,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
field.validators = [
|
field.validators = [
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex=self.validation_regex,
|
regex=self.validation_regex,
|
||||||
message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(regex=self.validation_regex))
|
message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(
|
||||||
|
regex=self.validation_regex
|
||||||
|
))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -577,9 +587,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
if type(value) is not int:
|
if type(value) is not int:
|
||||||
raise ValidationError(_("Value must be an integer."))
|
raise ValidationError(_("Value must be an integer."))
|
||||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||||
raise ValidationError(_("Value must be at least {validation_minimum}").format(validation_minimum=self.validation_maximum))
|
raise ValidationError(
|
||||||
|
_("Value must be at least {minimum}").format(minimum=self.validation_maximum)
|
||||||
|
)
|
||||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
raise ValidationError(_("Value must not exceed {validation_maximum}").format(validation_maximum=self.validation_maximum))
|
raise ValidationError(
|
||||||
|
_("Value must not exceed {maximum}").format(maximum=self.validation_maximum)
|
||||||
|
)
|
||||||
|
|
||||||
# Validate decimal
|
# Validate decimal
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
@ -588,9 +602,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
raise ValidationError(_("Value must be a decimal."))
|
raise ValidationError(_("Value must be a decimal."))
|
||||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||||
raise ValidationError(_("Value must be at least {validation_minimum}").format(validation_minimum=self.validation_minimum))
|
raise ValidationError(
|
||||||
|
_("Value must be at least {minimum}").format(minimum=self.validation_minimum)
|
||||||
|
)
|
||||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
raise ValidationError(_("Value must not exceed {validation_maximum}").format(validation_maximum=self.validation_maximum))
|
raise ValidationError(
|
||||||
|
_("Value must not exceed {maximum}").format(maximum=self.validation_maximum)
|
||||||
|
)
|
||||||
|
|
||||||
# Validate boolean
|
# Validate boolean
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||||
@ -610,13 +628,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
try:
|
try:
|
||||||
datetime.fromisoformat(value)
|
datetime.fromisoformat(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError(_("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."))
|
raise ValidationError(
|
||||||
|
_("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).")
|
||||||
|
)
|
||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
if value not in self.choices:
|
if value not in self.choices:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("Invalid choice ({value}). Available choices are: {choices}").format(value=value, choices=', '.join(self.choices))
|
_("Invalid choice ({value}). Available choices are: {choices}").format(
|
||||||
|
value=value, choices=', '.join(self.choices)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate all selected choices
|
# Validate all selected choices
|
||||||
@ -635,7 +657,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
# Validate selected objects
|
# Validate selected objects
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
if type(value) is not list:
|
if type(value) is not list:
|
||||||
raise ValidationError(_("Value must be a list of object IDs, not {type}").format(type=type(value).__name__))
|
raise ValidationError(
|
||||||
|
_("Value must be a list of object IDs, not {type}").format(type=type(value).__name__)
|
||||||
|
)
|
||||||
for id in value:
|
for id in value:
|
||||||
if type(id) is not int:
|
if type(id) is not int:
|
||||||
raise ValidationError(_("Found invalid object ID: {id}").format(id=id))
|
raise ValidationError(_("Found invalid object ID: {id}").format(id=id))
|
||||||
|
@ -48,7 +48,7 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
content_types = models.ManyToManyField(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
related_name='webhooks',
|
related_name='webhooks',
|
||||||
verbose_name='Object types',
|
verbose_name=_('object types'),
|
||||||
limit_choices_to=FeatureQuery('webhooks'),
|
limit_choices_to=FeatureQuery('webhooks'),
|
||||||
help_text=_("The object(s) to which this Webhook applies.")
|
help_text=_("The object(s) to which this Webhook applies.")
|
||||||
)
|
)
|
||||||
@ -58,35 +58,37 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
type_create = models.BooleanField(
|
type_create = models.BooleanField(
|
||||||
verbose_name=_('type create'),
|
verbose_name=_('on create'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a matching object is created.")
|
help_text=_("Triggers when a matching object is created.")
|
||||||
)
|
)
|
||||||
type_update = models.BooleanField(
|
type_update = models.BooleanField(
|
||||||
verbose_name=_('type update'),
|
verbose_name=_('on update'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a matching object is updated.")
|
help_text=_("Triggers when a matching object is updated.")
|
||||||
)
|
)
|
||||||
type_delete = models.BooleanField(
|
type_delete = models.BooleanField(
|
||||||
verbose_name=_('type delete'),
|
verbose_name=_('on delete'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a matching object is deleted.")
|
help_text=_("Triggers when a matching object is deleted.")
|
||||||
)
|
)
|
||||||
type_job_start = models.BooleanField(
|
type_job_start = models.BooleanField(
|
||||||
verbose_name=_('type job start'),
|
verbose_name=_('on job start'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a job for a matching object is started.")
|
help_text=_("Triggers when a job for a matching object is started.")
|
||||||
)
|
)
|
||||||
type_job_end = models.BooleanField(
|
type_job_end = models.BooleanField(
|
||||||
verbose_name=_('type job end'),
|
verbose_name=_('on job end'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a job for a matching object terminates.")
|
help_text=_("Triggers when a job for a matching object terminates.")
|
||||||
)
|
)
|
||||||
payload_url = models.CharField(
|
payload_url = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
verbose_name=_('URL'),
|
verbose_name=_('URL'),
|
||||||
help_text=_('This URL will be called using the HTTP method defined when the webhook is called. '
|
help_text=_(
|
||||||
'Jinja2 template processing is supported with the same context as the request body.')
|
"This URL will be called using the HTTP method defined when the webhook is called. Jinja2 template "
|
||||||
|
"processing is supported with the same context as the request body."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
verbose_name=_('enabled'),
|
verbose_name=_('enabled'),
|
||||||
@ -102,31 +104,37 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
default=HTTP_CONTENT_TYPE_JSON,
|
default=HTTP_CONTENT_TYPE_JSON,
|
||||||
verbose_name=_('HTTP content type'),
|
verbose_name=_('HTTP content type'),
|
||||||
help_text=_('The complete list of official content types is available '
|
help_text=_(
|
||||||
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.')
|
'The complete list of official content types is available '
|
||||||
|
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
additional_headers = models.TextField(
|
additional_headers = models.TextField(
|
||||||
verbose_name=_('additional headers'),
|
verbose_name=_('additional headers'),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
|
help_text=_(
|
||||||
"Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
|
"User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. Headers "
|
||||||
"supported with the same context as the request body (below).")
|
"should be defined in the format <code>Name: Value</code>. Jinja2 template processing is supported with "
|
||||||
|
"the same context as the request body (below)."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
body_template = models.TextField(
|
body_template = models.TextField(
|
||||||
verbose_name=_('body template'),
|
verbose_name=_('body template'),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Jinja2 template for a custom request body. If blank, a JSON object representing the change will be '
|
help_text=_(
|
||||||
'included. Available context data includes: <code>event</code>, <code>model</code>, '
|
"Jinja2 template for a custom request body. If blank, a JSON object representing the change will be "
|
||||||
'<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.')
|
"included. Available context data includes: <code>event</code>, <code>model</code>, "
|
||||||
|
"<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
secret = models.CharField(
|
secret = models.CharField(
|
||||||
verbose_name=_('secret'),
|
verbose_name=_('secret'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("When provided, the request will include a 'X-Hook-Signature' "
|
help_text=_(
|
||||||
"header containing a HMAC hex digest of the payload body using "
|
"When provided, the request will include a <code>X-Hook-Signature</code> header containing a HMAC hex "
|
||||||
"the secret as the key. The secret is not transmitted in "
|
"digest of the payload body using the secret as the key. The secret is not transmitted in the request."
|
||||||
"the request.")
|
)
|
||||||
)
|
)
|
||||||
conditions = models.JSONField(
|
conditions = models.JSONField(
|
||||||
verbose_name=_('conditions'),
|
verbose_name=_('conditions'),
|
||||||
@ -144,8 +152,9 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('CA File Path'),
|
verbose_name=_('CA File Path'),
|
||||||
help_text=_('The specific CA certificate file to use for SSL verification. '
|
help_text=_(
|
||||||
'Leave blank to use the system defaults.')
|
"The specific CA certificate file to use for SSL verification. Leave blank to use the system defaults."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -243,7 +252,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_("Jinja2 template code for link text")
|
help_text=_("Jinja2 template code for link text")
|
||||||
)
|
)
|
||||||
link_url = models.TextField(
|
link_url = models.TextField(
|
||||||
verbose_name=_('Link URL'),
|
verbose_name=_('link URL'),
|
||||||
help_text=_("Jinja2 template code for link URL")
|
help_text=_("Jinja2 template code for link URL")
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
@ -333,8 +342,10 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
template_code = models.TextField(
|
template_code = models.TextField(
|
||||||
help_text=_('Jinja2 template code. The list of objects being exported is passed as a context variable named '
|
help_text=_(
|
||||||
'<code>queryset</code>.')
|
"Jinja2 template code. The list of objects being exported is passed as a context variable named "
|
||||||
|
"<code>queryset</code>."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
mime_type = models.CharField(
|
mime_type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -626,7 +637,9 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||||||
# Prevent the creation of journal entries on unsupported models
|
# Prevent the creation of journal entries on unsupported models
|
||||||
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
||||||
if self.assigned_object_type not in permitted_types:
|
if self.assigned_object_type not in permitted_types:
|
||||||
raise ValidationError(_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type))
|
raise ValidationError(
|
||||||
|
_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type)
|
||||||
|
)
|
||||||
|
|
||||||
def get_kind_color(self):
|
def get_kind_color(self):
|
||||||
return JournalEntryKindChoices.colors.get(self.kind)
|
return JournalEntryKindChoices.colors.get(self.kind)
|
||||||
@ -687,7 +700,7 @@ class ConfigRevision(models.Model):
|
|||||||
data = models.JSONField(
|
data = models.JSONField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Configuration data')
|
verbose_name=_('configuration data')
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
@ -68,7 +68,11 @@ class ASNRange(OrganizationalModel):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.end <= self.start:
|
if self.end <= self.start:
|
||||||
raise ValidationError(_("Starting ASN ({start}) must be lower than ending ASN ({end}).").format(start=self.start, end=self.end))
|
raise ValidationError(
|
||||||
|
_("Starting ASN ({start}) must be lower than ending ASN ({end}).").format(
|
||||||
|
start=self.start, end=self.end
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_child_asns(self):
|
def get_child_asns(self):
|
||||||
return ASN.objects.filter(
|
return ASN.objects.filter(
|
||||||
|
@ -20,7 +20,7 @@ class FHRPGroup(PrimaryModel):
|
|||||||
A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
|
A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
|
||||||
"""
|
"""
|
||||||
group_id = models.PositiveSmallIntegerField(
|
group_id = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('Group ID')
|
verbose_name=_('group ID')
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
@ -36,12 +36,12 @@ class FHRPGroup(PrimaryModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
choices=FHRPGroupAuthTypeChoices,
|
choices=FHRPGroupAuthTypeChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Authentication type')
|
verbose_name=_('authentication type')
|
||||||
)
|
)
|
||||||
auth_key = models.CharField(
|
auth_key = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Authentication key')
|
verbose_name=_('authentication key')
|
||||||
)
|
)
|
||||||
ip_addresses = GenericRelation(
|
ip_addresses = GenericRelation(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
|
@ -59,7 +59,7 @@ class RIR(OrganizationalModel):
|
|||||||
"""
|
"""
|
||||||
is_private = models.BooleanField(
|
is_private = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Private'),
|
verbose_name=_('private'),
|
||||||
help_text=_('IP space managed by this RIR is considered private')
|
help_text=_('IP space managed by this RIR is considered private')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -135,9 +135,9 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
||||||
if covering_aggregates:
|
if covering_aggregates:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'prefix': _("Aggregates cannot overlap. {} is already covered by an existing aggregate ({}).").format(
|
'prefix': _(
|
||||||
self.prefix, covering_aggregates[0]
|
"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
|
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||||
@ -231,14 +231,13 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='prefixes',
|
related_name='prefixes',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True
|
||||||
verbose_name='VLAN'
|
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PrefixStatusChoices,
|
choices=PrefixStatusChoices,
|
||||||
default=PrefixStatusChoices.STATUS_ACTIVE,
|
default=PrefixStatusChoices.STATUS_ACTIVE,
|
||||||
verbose_name=_('Status'),
|
verbose_name=_('status'),
|
||||||
help_text=_('Operational status of this prefix')
|
help_text=_('Operational status of this prefix')
|
||||||
)
|
)
|
||||||
role = models.ForeignKey(
|
role = models.ForeignKey(
|
||||||
@ -250,7 +249,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
help_text=_('The primary function of this prefix')
|
help_text=_('The primary function of this prefix')
|
||||||
)
|
)
|
||||||
is_pool = models.BooleanField(
|
is_pool = models.BooleanField(
|
||||||
verbose_name=_('Is a pool'),
|
verbose_name=_('is a pool'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_('All IP addresses within this prefix are considered usable')
|
help_text=_('All IP addresses within this prefix are considered usable')
|
||||||
)
|
)
|
||||||
@ -548,23 +547,33 @@ class IPRange(PrimaryModel):
|
|||||||
# Check that start & end IP versions match
|
# Check that start & end IP versions match
|
||||||
if self.start_address.version != self.end_address.version:
|
if self.start_address.version != self.end_address.version:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'end_address': _("Ending address version (IPv{end_address_version}) does not match starting "
|
'end_address': _(
|
||||||
"address (IPv{start_address_version})").format(
|
"Ending address version (IPv{end_address_version}) does not match starting address "
|
||||||
end_address_version=self.end_address.version, start_address_version=self.start_address.version)
|
"(IPv{start_address_version})"
|
||||||
|
).format(
|
||||||
|
end_address_version=self.end_address.version,
|
||||||
|
start_address_version=self.start_address.version
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Check that the start & end IP prefix lengths match
|
# Check that the start & end IP prefix lengths match
|
||||||
if self.start_address.prefixlen != self.end_address.prefixlen:
|
if self.start_address.prefixlen != self.end_address.prefixlen:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'end_address': _("Ending address mask (/{end_address_prefixlen}) does not match starting "
|
'end_address': _(
|
||||||
"address mask (/{start_address_prefixlen})").format(
|
"Ending address mask (/{end_address_prefixlen}) does not match starting address mask "
|
||||||
end_address_prefixlen=self.end_address.prefixlen, start_address_prefixlen=self.start_address.prefixlen)
|
"(/{start_address_prefixlen})"
|
||||||
|
).format(
|
||||||
|
end_address_prefixlen=self.end_address.prefixlen,
|
||||||
|
start_address_prefixlen=self.start_address.prefixlen
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Check that the ending address is greater than the starting address
|
# Check that the ending address is greater than the starting address
|
||||||
if not self.end_address > self.start_address:
|
if not self.end_address > self.start_address:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'end_address': _("Ending address must be lower than the starting address ({start_address})").format(start_address=self.start_address)
|
'end_address': _(
|
||||||
|
"Ending address must be lower than the starting address ({start_address})"
|
||||||
|
).format(start_address=self.start_address)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Check for overlapping ranges
|
# Check for overlapping ranges
|
||||||
@ -574,13 +583,18 @@ class IPRange(PrimaryModel):
|
|||||||
Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside
|
Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside
|
||||||
).first()
|
).first()
|
||||||
if overlapping_range:
|
if overlapping_range:
|
||||||
raise ValidationError(_("Defined addresses overlap with range {overlapping_range} in VRF {vrf}").format(
|
raise ValidationError(
|
||||||
overlapping_range=overlapping_range, vrf=self.vrf))
|
_("Defined addresses overlap with range {overlapping_range} in VRF {vrf}").format(
|
||||||
|
overlapping_range=overlapping_range,
|
||||||
|
vrf=self.vrf
|
||||||
|
))
|
||||||
|
|
||||||
# Validate maximum size
|
# Validate maximum size
|
||||||
MAX_SIZE = 2 ** 32 - 1
|
MAX_SIZE = 2 ** 32 - 1
|
||||||
if int(self.end_address.ip - self.start_address.ip) + 1 > MAX_SIZE:
|
if int(self.end_address.ip - self.start_address.ip) + 1 > MAX_SIZE:
|
||||||
raise ValidationError(_("Defined range exceeds maximum supported size ({max_size})").format(max_size=MAX_SIZE))
|
raise ValidationError(
|
||||||
|
_("Defined range exceeds maximum supported size ({max_size})").format(max_size=MAX_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -745,14 +759,14 @@ class IPAddress(PrimaryModel):
|
|||||||
related_name='nat_outside',
|
related_name='nat_outside',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('NAT (Inside)'),
|
verbose_name=_('NAT (inside)'),
|
||||||
help_text=_('The IP for which this address is the "outside" IP')
|
help_text=_('The IP for which this address is the "outside" IP')
|
||||||
)
|
)
|
||||||
dns_name = models.CharField(
|
dns_name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[DNSValidator],
|
validators=[DNSValidator],
|
||||||
verbose_name=_('DNS Name'),
|
verbose_name=_('DNS name'),
|
||||||
help_text=_('Hostname or FQDN (not case-sensitive)')
|
help_text=_('Hostname or FQDN (not case-sensitive)')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -128,7 +128,11 @@ class L2VPNTermination(NetBoxModel):
|
|||||||
obj_type = ContentType.objects.get_for_model(self.assigned_object)
|
obj_type = ContentType.objects.get_for_model(self.assigned_object)
|
||||||
if L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type).\
|
if L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type).\
|
||||||
exclude(pk=self.pk).count() > 0:
|
exclude(pk=self.pk).count() > 0:
|
||||||
raise ValidationError(_('L2VPN Termination already assigned ({assigned_object})').format(assigned_object=self.assigned_object))
|
raise ValidationError(
|
||||||
|
_('L2VPN Termination already assigned ({assigned_object})').format(
|
||||||
|
assigned_object=self.assigned_object
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Only check if L2VPN is set and is of type P2P
|
# Only check if L2VPN is set and is of type P2P
|
||||||
if hasattr(self, 'l2vpn') and self.l2vpn.type in L2VPNTypeChoices.P2P:
|
if hasattr(self, 'l2vpn') and self.l2vpn.type in L2VPNTypeChoices.P2P:
|
||||||
@ -136,9 +140,10 @@ class L2VPNTermination(NetBoxModel):
|
|||||||
if terminations_count >= 2:
|
if terminations_count >= 2:
|
||||||
l2vpn_type = self.l2vpn.get_type_display()
|
l2vpn_type = self.l2vpn.get_type_display()
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} already '
|
_(
|
||||||
'defined.').format(l2vpn_type=l2vpn_type, terminations_count=terminations_count)
|
'{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} '
|
||||||
)
|
'already defined.'
|
||||||
|
).format(l2vpn_type=l2vpn_type, terminations_count=terminations_count))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def assigned_object_parent(self):
|
def assigned_object_parent(self):
|
||||||
|
@ -30,7 +30,7 @@ class ServiceBase(models.Model):
|
|||||||
MaxValueValidator(SERVICE_PORT_MAX)
|
MaxValueValidator(SERVICE_PORT_MAX)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
verbose_name=_('Port numbers')
|
verbose_name=_('port numbers')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -82,7 +82,8 @@ class Service(ServiceBase, PrimaryModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
verbose_name=_('name')
|
||||||
)
|
)
|
||||||
ipaddresses = models.ManyToManyField(
|
ipaddresses = models.ManyToManyField(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
|
@ -47,7 +47,7 @@ class VLANGroup(OrganizationalModel):
|
|||||||
fk_field='scope_id'
|
fk_field='scope_id'
|
||||||
)
|
)
|
||||||
min_vid = models.PositiveSmallIntegerField(
|
min_vid = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('Minimum VLAN ID'),
|
verbose_name=_('minimum VLAN ID'),
|
||||||
default=VLAN_VID_MIN,
|
default=VLAN_VID_MIN,
|
||||||
validators=(
|
validators=(
|
||||||
MinValueValidator(VLAN_VID_MIN),
|
MinValueValidator(VLAN_VID_MIN),
|
||||||
@ -56,7 +56,7 @@ class VLANGroup(OrganizationalModel):
|
|||||||
help_text=_('Lowest permissible ID of a child VLAN')
|
help_text=_('Lowest permissible ID of a child VLAN')
|
||||||
)
|
)
|
||||||
max_vid = models.PositiveSmallIntegerField(
|
max_vid = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('Maximum VLAN ID'),
|
verbose_name=_('maximum VLAN ID'),
|
||||||
default=VLAN_VID_MAX,
|
default=VLAN_VID_MAX,
|
||||||
validators=(
|
validators=(
|
||||||
MinValueValidator(VLAN_VID_MIN),
|
MinValueValidator(VLAN_VID_MIN),
|
||||||
@ -145,7 +145,7 @@ class VLAN(PrimaryModel):
|
|||||||
help_text=_("VLAN group (optional)")
|
help_text=_("VLAN group (optional)")
|
||||||
)
|
)
|
||||||
vid = models.PositiveSmallIntegerField(
|
vid = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('ID'),
|
verbose_name=_('VLAN ID'),
|
||||||
validators=(
|
validators=(
|
||||||
MinValueValidator(VLAN_VID_MIN),
|
MinValueValidator(VLAN_VID_MIN),
|
||||||
MaxValueValidator(VLAN_VID_MAX)
|
MaxValueValidator(VLAN_VID_MAX)
|
||||||
@ -219,15 +219,17 @@ class VLAN(PrimaryModel):
|
|||||||
# Validate VLAN group (if assigned)
|
# Validate VLAN group (if assigned)
|
||||||
if self.group and self.site and self.group.scope != self.site:
|
if self.group and self.site and self.group.scope != self.site:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'group': _("VLAN is assigned to group {group} (scope: {scope}); cannot also assign to "
|
'group': _(
|
||||||
"site {site}.").format(group=self.group, scope=self.group.scope, site=self.site)
|
"VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
|
||||||
|
).format(group=self.group, scope=self.group.scope, site=self.site)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate group min/max VIDs
|
# Validate group min/max VIDs
|
||||||
if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
|
if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'vid': _("VID must be between {min_vid} and {max_vid} for VLANs in group "
|
'vid': _(
|
||||||
"{group}").format(min_vid=self.group.min_vid, max_vid=self.group.max_vid, group=self.group)
|
"VID must be between {min_vid} and {max_vid} for VLANs in group {group}"
|
||||||
|
).format(min_vid=self.group.min_vid, max_vid=self.group.max_vid, group=self.group)
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
|
@ -27,7 +27,7 @@ class VRF(PrimaryModel):
|
|||||||
unique=True,
|
unique=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Route distinguisher'),
|
verbose_name=_('route distinguisher'),
|
||||||
help_text=_('Unique route distinguisher (as defined in RFC 4364)')
|
help_text=_('Unique route distinguisher (as defined in RFC 4364)')
|
||||||
)
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
@ -39,7 +39,7 @@ class VRF(PrimaryModel):
|
|||||||
)
|
)
|
||||||
enforce_unique = models.BooleanField(
|
enforce_unique = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_('Enforce unique space'),
|
verbose_name=_('enforce unique space'),
|
||||||
help_text=_('Prevent duplicate prefixes/IP addresses within this VRF')
|
help_text=_('Prevent duplicate prefixes/IP addresses within this VRF')
|
||||||
)
|
)
|
||||||
import_targets = models.ManyToManyField(
|
import_targets = models.ManyToManyField(
|
||||||
|
@ -152,7 +152,7 @@ class NestedGroupModel(CloningMixin, NetBoxFeatureSet, MPTTModel):
|
|||||||
# An MPTT model cannot be its own parent
|
# An MPTT model cannot be its own parent
|
||||||
if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
|
if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
|
"parent": "Cannot assign self or child {type} as parent.".format(type=self._meta.verbose_name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,7 +146,6 @@ class CustomFieldsMixin(models.Model):
|
|||||||
Enables support for custom fields.
|
Enables support for custom fields.
|
||||||
"""
|
"""
|
||||||
custom_field_data = models.JSONField(
|
custom_field_data = models.JSONField(
|
||||||
verbose_name=_('custom field data'),
|
|
||||||
encoder=CustomFieldJSONEncoder,
|
encoder=CustomFieldJSONEncoder,
|
||||||
blank=True,
|
blank=True,
|
||||||
default=dict
|
default=dict
|
||||||
|
@ -105,7 +105,6 @@ class UserConfig(models.Model):
|
|||||||
related_name='config'
|
related_name='config'
|
||||||
)
|
)
|
||||||
data = models.JSONField(
|
data = models.JSONField(
|
||||||
verbose_name=_('data'),
|
|
||||||
default=dict
|
default=dict
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -177,7 +176,9 @@ class UserConfig(models.Model):
|
|||||||
d = d[key]
|
d = d[key]
|
||||||
elif key in d:
|
elif key in d:
|
||||||
err_path = '.'.join(path.split('.')[:i + 1])
|
err_path = '.'.join(path.split('.')[:i + 1])
|
||||||
raise TypeError(_("Key '{err_path}' is a leaf node; cannot assign new keys").format(err_path=err_path))
|
raise TypeError(
|
||||||
|
_("Key '{err_path}' is a leaf node; cannot assign new keys").format(err_path=err_path)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
d = d.setdefault(key, {})
|
d = d.setdefault(key, {})
|
||||||
|
|
||||||
@ -187,7 +188,9 @@ class UserConfig(models.Model):
|
|||||||
if type(value) is dict:
|
if type(value) is dict:
|
||||||
d[key].update(value)
|
d[key].update(value)
|
||||||
else:
|
else:
|
||||||
raise TypeError(_("Key '{path}' is a dictionary; cannot assign a non-dictionary value").format(path=path))
|
raise TypeError(
|
||||||
|
_("Key '{path}' is a dictionary; cannot assign a non-dictionary value").format(path=path)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
d[key] = value
|
d[key] = value
|
||||||
|
|
||||||
@ -280,7 +283,7 @@ class Token(models.Model):
|
|||||||
base_field=IPNetworkField(),
|
base_field=IPNetworkField(),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Allowed IPs'),
|
verbose_name=_('allowed IPs'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
|
'Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
|
||||||
'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"'
|
'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"'
|
||||||
@ -385,6 +388,7 @@ class ObjectPermission(models.Model):
|
|||||||
constraints = models.JSONField(
|
constraints = models.JSONField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
verbose_name=_('constraints'),
|
||||||
help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
|
help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
choices=VirtualMachineStatusChoices,
|
choices=VirtualMachineStatusChoices,
|
||||||
default=VirtualMachineStatusChoices.STATUS_ACTIVE,
|
default=VirtualMachineStatusChoices.STATUS_ACTIVE,
|
||||||
verbose_name=_('Status')
|
verbose_name=_('status')
|
||||||
)
|
)
|
||||||
role = models.ForeignKey(
|
role = models.ForeignKey(
|
||||||
to='dcim.DeviceRole',
|
to='dcim.DeviceRole',
|
||||||
@ -92,7 +92,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='Primary IPv4'
|
verbose_name='primary IPv4'
|
||||||
)
|
)
|
||||||
primary_ip6 = models.OneToOneField(
|
primary_ip6 = models.OneToOneField(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
@ -100,7 +100,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='Primary IPv6'
|
verbose_name='primary IPv6'
|
||||||
)
|
)
|
||||||
vcpus = models.DecimalField(
|
vcpus = models.DecimalField(
|
||||||
max_digits=6,
|
max_digits=6,
|
||||||
@ -115,12 +115,12 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
memory = models.PositiveIntegerField(
|
memory = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Memory (MB)')
|
verbose_name=_('memory (MB)')
|
||||||
)
|
)
|
||||||
disk = models.PositiveIntegerField(
|
disk = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('Disk (GB)')
|
verbose_name=_('disk (GB)')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Counter fields
|
# Counter fields
|
||||||
@ -176,7 +176,9 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
# Validate site for cluster & device
|
# Validate site for cluster & device
|
||||||
if self.cluster and self.site and self.cluster.site != self.site:
|
if self.cluster and self.site and self.cluster.site != self.site:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'cluster': _('The selected cluster ({cluster}) is not assigned to this site ({site}).').format(cluster=self.cluster, site=self.site)
|
'cluster': _(
|
||||||
|
'The selected cluster ({cluster}) is not assigned to this site ({site}).'
|
||||||
|
).format(cluster=self.cluster, site=self.site)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate assigned cluster device
|
# Validate assigned cluster device
|
||||||
@ -186,7 +188,9 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
})
|
})
|
||||||
if self.device and self.device not in self.cluster.devices.all():
|
if self.device and self.device not in self.cluster.devices.all():
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'device': _('The selected device ({device}) is not assigned to this cluster ({cluster}).').format(device=self.device, cluster=self.cluster)
|
'device': _(
|
||||||
|
"The selected device ({device}) is not assigned to this cluster ({cluster})."
|
||||||
|
).format(device=self.device, cluster=self.cluster)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate primary IP addresses
|
# Validate primary IP addresses
|
||||||
@ -197,8 +201,9 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
if ip is not None:
|
if ip is not None:
|
||||||
if ip.address.version != family:
|
if ip.address.version != family:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
field: _("Must be an IPv{family} address. ({ip} is an IPv{version} address.)").format(
|
field: _(
|
||||||
family=family, ip=ip, version=ip.address.version),
|
"Must be an IPv{family} address. ({ip} is an IPv{version} address.)"
|
||||||
|
).format(family=family, ip=ip, version=ip.address.version)
|
||||||
})
|
})
|
||||||
if ip.assigned_object in interfaces:
|
if ip.assigned_object in interfaces:
|
||||||
pass
|
pass
|
||||||
@ -259,13 +264,13 @@ class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
|
|||||||
related_name='vminterfaces_as_untagged',
|
related_name='vminterfaces_as_untagged',
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Untagged VLAN')
|
verbose_name=_('untagged VLAN')
|
||||||
)
|
)
|
||||||
tagged_vlans = models.ManyToManyField(
|
tagged_vlans = models.ManyToManyField(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
related_name='vminterfaces_as_tagged',
|
related_name='vminterfaces_as_tagged',
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Tagged VLANs')
|
verbose_name=_('tagged VLANs')
|
||||||
)
|
)
|
||||||
ip_addresses = GenericRelation(
|
ip_addresses = GenericRelation(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
@ -322,8 +327,10 @@ class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
|
|||||||
# An interface's parent must belong to the same virtual machine
|
# An interface's parent must belong to the same virtual machine
|
||||||
if self.parent and self.parent.virtual_machine != self.virtual_machine:
|
if self.parent and self.parent.virtual_machine != self.virtual_machine:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'parent': _("The selected parent interface ({parent}) belongs to a different virtual machine "
|
'parent': _(
|
||||||
"({virtual_machine}).").format(parent=self.parent, virtual_machine=self.parent.virtual_machine)
|
"The selected parent interface ({parent}) belongs to a different virtual machine "
|
||||||
|
"({virtual_machine})."
|
||||||
|
).format(parent=self.parent, virtual_machine=self.parent.virtual_machine)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Bridge validation
|
# Bridge validation
|
||||||
@ -335,8 +342,10 @@ class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
|
|||||||
# A bridged interface belong to the same virtual machine
|
# A bridged interface belong to the same virtual machine
|
||||||
if self.bridge and self.bridge.virtual_machine != self.virtual_machine:
|
if self.bridge and self.bridge.virtual_machine != self.virtual_machine:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'bridge': _("The selected bridge interface ({bridge}) belongs to a different virtual machine "
|
'bridge': _(
|
||||||
"({virtual_machine}).").format(bridge=self.bridge, virtual_machine=self.bridge.virtual_machine)
|
"The selected bridge interface ({bridge}) belongs to a different virtual machine "
|
||||||
|
"({virtual_machine})."
|
||||||
|
).format(bridge=self.bridge, virtual_machine=self.bridge.virtual_machine)
|
||||||
})
|
})
|
||||||
|
|
||||||
# VLAN validation
|
# VLAN validation
|
||||||
@ -344,8 +353,10 @@ class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
|
|||||||
# Validate untagged VLAN
|
# Validate untagged VLAN
|
||||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
|
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'untagged_vlan': _("The untagged VLAN ({untagged_vlan}) must belong to the same site as the "
|
'untagged_vlan': _(
|
||||||
"interface's parent virtual machine, or it must be global.").format(untagged_vlan=self.untagged_vlan)
|
"The untagged VLAN ({untagged_vlan}) must belong to the same site as the interface's parent "
|
||||||
|
"virtual machine, or it must be global."
|
||||||
|
).format(untagged_vlan=self.untagged_vlan)
|
||||||
})
|
})
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
|
@ -25,10 +25,10 @@ class WirelessAuthenticationBase(models.Model):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
choices=WirelessAuthTypeChoices,
|
choices=WirelessAuthTypeChoices,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Auth Type"),
|
verbose_name=_("authentication type"),
|
||||||
)
|
)
|
||||||
auth_cipher = models.CharField(
|
auth_cipher = models.CharField(
|
||||||
verbose_name=_('auth cipher'),
|
verbose_name=_('authentication cipher'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=WirelessAuthCipherChoices,
|
choices=WirelessAuthCipherChoices,
|
||||||
blank=True
|
blank=True
|
||||||
@ -36,7 +36,7 @@ class WirelessAuthenticationBase(models.Model):
|
|||||||
auth_psk = models.CharField(
|
auth_psk = models.CharField(
|
||||||
max_length=PSK_MAX_LENGTH,
|
max_length=PSK_MAX_LENGTH,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('Pre-shared key')
|
verbose_name=_('pre-shared key')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -90,7 +90,8 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
|
|||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=WirelessLANStatusChoices,
|
choices=WirelessLANStatusChoices,
|
||||||
default=WirelessLANStatusChoices.STATUS_ACTIVE
|
default=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||||
|
verbose_name=_('status')
|
||||||
)
|
)
|
||||||
vlan = models.ForeignKey(
|
vlan = models.ForeignKey(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
@ -138,14 +139,14 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
|
|||||||
limit_choices_to=get_wireless_interface_types,
|
limit_choices_to=get_wireless_interface_types,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
verbose_name=_('Interface A'),
|
verbose_name=_('interface A'),
|
||||||
)
|
)
|
||||||
interface_b = models.ForeignKey(
|
interface_b = models.ForeignKey(
|
||||||
to='dcim.Interface',
|
to='dcim.Interface',
|
||||||
limit_choices_to=get_wireless_interface_types,
|
limit_choices_to=get_wireless_interface_types,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
verbose_name=_('Interface B'),
|
verbose_name=_('interface B'),
|
||||||
)
|
)
|
||||||
ssid = models.CharField(
|
ssid = models.CharField(
|
||||||
max_length=SSID_MAX_LENGTH,
|
max_length=SSID_MAX_LENGTH,
|
||||||
@ -208,11 +209,15 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
|
|||||||
# Validate interface types
|
# Validate interface types
|
||||||
if self.interface_a.type not in WIRELESS_IFACE_TYPES:
|
if self.interface_a.type not in WIRELESS_IFACE_TYPES:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface_a': _("{type_display} is not a wireless interface.").format(type_display=self.interface_a.get_type_display())
|
'interface_a': _(
|
||||||
|
"{type_display} is not a wireless interface."
|
||||||
|
).format(type_display=self.interface_a.get_type_display())
|
||||||
})
|
})
|
||||||
if self.interface_b.type not in WIRELESS_IFACE_TYPES:
|
if self.interface_b.type not in WIRELESS_IFACE_TYPES:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface_a': _("{type_display} is not a wireless interface.").format(type_display=self.interface_b.get_type_display())
|
'interface_a': _(
|
||||||
|
"{type_display} is not a wireless interface."
|
||||||
|
).format(type_display=self.interface_b.get_type_display())
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
Loading…
Reference in New Issue
Block a user