Merge branch 'feature' into 10348-decimal-custom-field

This commit is contained in:
Arthur 2022-09-28 11:42:37 -07:00
commit 2c81fc2c56
38 changed files with 874 additions and 254 deletions

View File

@ -19,6 +19,10 @@ The wire protocol employed by cooperating servers to maintain the virtual [IP ad
The group's numeric identifier. The group's numeric identifier.
### Name
An optional name for the FHRP group.
### Authentication Type ### Authentication Type
The type of authentication employed by group nodes, if any. The type of authentication employed by group nodes, if any.

View File

@ -3,6 +3,10 @@
!!! warning "PostgreSQL 11 Required" !!! warning "PostgreSQL 11 Required"
NetBox v3.4 requires PostgreSQL 11 or later. NetBox v3.4 requires PostgreSQL 11 or later.
### Enhancements
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
### Plugins API ### Plugins API
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin * [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
@ -10,3 +14,8 @@
### Other Changes ### Other Changes
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11 * [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
### REST API Changes
* ipam.FHRPGroup
* Added optional `name` field

View File

@ -0,0 +1,39 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0038_cabling_cleanup'),
]
operations = [
migrations.RemoveConstraint(
model_name='providernetwork',
name='circuits_providernetwork_provider_name',
),
migrations.AlterUniqueTogether(
name='circuit',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='circuittermination',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='providernetwork',
unique_together=set(),
),
migrations.AddConstraint(
model_name='circuit',
constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'),
),
migrations.AddConstraint(
model_name='circuittermination',
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
),
]

View File

@ -132,7 +132,12 @@ class Circuit(NetBoxModel):
class Meta: class Meta:
ordering = ['provider', 'cid'] ordering = ['provider', 'cid']
unique_together = ['provider', 'cid'] constraints = (
models.UniqueConstraint(
fields=('provider', 'cid'),
name='%(app_label)s_%(class)s_unique_provider_cid'
),
)
def __str__(self): def __str__(self):
return self.cid return self.cid
@ -208,7 +213,12 @@ class CircuitTermination(
class Meta: class Meta:
ordering = ['circuit', 'term_side'] ordering = ['circuit', 'term_side']
unique_together = ['circuit', 'term_side'] constraints = (
models.UniqueConstraint(
fields=('circuit', 'term_side'),
name='%(app_label)s_%(class)s_unique_circuit_term_side'
),
)
def __str__(self): def __str__(self):
return f'Termination {self.term_side}: {self.site or self.provider_network}' return f'Termination {self.term_side}: {self.site or self.provider_network}'

View File

@ -106,10 +106,9 @@ class ProviderNetwork(NetBoxModel):
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('provider', 'name'), fields=('provider', 'name'),
name='circuits_providernetwork_provider_name' name='%(app_label)s_%(class)s_unique_provider_name'
), ),
) )
unique_together = ('provider', 'name')
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -0,0 +1,331 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0161_cabling_cleanup'),
]
operations = [
migrations.RemoveConstraint(
model_name='cabletermination',
name='dcim_cable_termination_unique_termination',
),
migrations.RemoveConstraint(
model_name='location',
name='dcim_location_name',
),
migrations.RemoveConstraint(
model_name='location',
name='dcim_location_slug',
),
migrations.RemoveConstraint(
model_name='region',
name='dcim_region_name',
),
migrations.RemoveConstraint(
model_name='region',
name='dcim_region_slug',
),
migrations.RemoveConstraint(
model_name='sitegroup',
name='dcim_sitegroup_name',
),
migrations.RemoveConstraint(
model_name='sitegroup',
name='dcim_sitegroup_slug',
),
migrations.AlterUniqueTogether(
name='consoleport',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='consoleporttemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='consoleserverport',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='consoleserverporttemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='device',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='devicebay',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='devicebaytemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='devicetype',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='frontport',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='frontporttemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='interface',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='interfacetemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='inventoryitem',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='inventoryitemtemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='modulebay',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='modulebaytemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='moduletype',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='powerfeed',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='poweroutlet',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='poweroutlettemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='powerpanel',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='powerport',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='powerporttemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='rack',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='rearport',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='rearporttemplate',
unique_together=set(),
),
migrations.AddConstraint(
model_name='cabletermination',
constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cabletermination_unique_termination'),
),
migrations.AddConstraint(
model_name='consoleport',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleport_unique_device_name'),
),
migrations.AddConstraint(
model_name='consoleporttemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_consoleporttemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='consoleporttemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_consoleporttemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='consoleserverport',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleserverport_unique_device_name'),
),
migrations.AddConstraint(
model_name='consoleserverporttemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_consoleserverporttemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='consoleserverporttemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_consoleserverporttemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='device',
constraint=models.UniqueConstraint(fields=('name', 'site', 'tenant'), name='dcim_device_unique_name_site_tenant'),
),
migrations.AddConstraint(
model_name='device',
constraint=models.UniqueConstraint(condition=models.Q(('tenant__isnull', True)), fields=('name', 'site'), name='dcim_device_unique_name_site', violation_error_message='Device name must be unique per site.'),
),
migrations.AddConstraint(
model_name='device',
constraint=models.UniqueConstraint(fields=('rack', 'position', 'face'), name='dcim_device_unique_rack_position_face'),
),
migrations.AddConstraint(
model_name='device',
constraint=models.UniqueConstraint(fields=('virtual_chassis', 'vc_position'), name='dcim_device_unique_virtual_chassis_vc_position'),
),
migrations.AddConstraint(
model_name='devicebay',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_devicebay_unique_device_name'),
),
migrations.AddConstraint(
model_name='devicebaytemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_devicebaytemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='devicetype',
constraint=models.UniqueConstraint(fields=('manufacturer', 'model'), name='dcim_devicetype_unique_manufacturer_model'),
),
migrations.AddConstraint(
model_name='devicetype',
constraint=models.UniqueConstraint(fields=('manufacturer', 'slug'), name='dcim_devicetype_unique_manufacturer_slug'),
),
migrations.AddConstraint(
model_name='frontport',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_frontport_unique_device_name'),
),
migrations.AddConstraint(
model_name='frontport',
constraint=models.UniqueConstraint(fields=('rear_port', 'rear_port_position'), name='dcim_frontport_unique_rear_port_position'),
),
migrations.AddConstraint(
model_name='frontporttemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_frontporttemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='frontporttemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_frontporttemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='frontporttemplate',
constraint=models.UniqueConstraint(fields=('rear_port', 'rear_port_position'), name='dcim_frontporttemplate_unique_rear_port_position'),
),
migrations.AddConstraint(
model_name='interface',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_interface_unique_device_name'),
),
migrations.AddConstraint(
model_name='interfacetemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_interfacetemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='interfacetemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_interfacetemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='inventoryitem',
constraint=models.UniqueConstraint(fields=('device', 'parent', 'name'), name='dcim_inventoryitem_unique_device_parent_name'),
),
migrations.AddConstraint(
model_name='inventoryitemtemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'parent', 'name'), name='dcim_inventoryitemtemplate_unique_device_type_parent_name'),
),
migrations.AddConstraint(
model_name='location',
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'name'), name='dcim_location_name', violation_error_message='A location with this name already exists within the specified site.'),
),
migrations.AddConstraint(
model_name='location',
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'slug'), name='dcim_location_slug', violation_error_message='A location with this slug already exists within the specified site.'),
),
migrations.AddConstraint(
model_name='modulebay',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_modulebay_unique_device_name'),
),
migrations.AddConstraint(
model_name='modulebaytemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_modulebaytemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='moduletype',
constraint=models.UniqueConstraint(fields=('manufacturer', 'model'), name='dcim_moduletype_unique_manufacturer_model'),
),
migrations.AddConstraint(
model_name='powerfeed',
constraint=models.UniqueConstraint(fields=('power_panel', 'name'), name='dcim_powerfeed_unique_power_panel_name'),
),
migrations.AddConstraint(
model_name='poweroutlet',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_poweroutlet_unique_device_name'),
),
migrations.AddConstraint(
model_name='poweroutlettemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_poweroutlettemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='poweroutlettemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_poweroutlettemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='powerpanel',
constraint=models.UniqueConstraint(fields=('site', 'name'), name='dcim_powerpanel_unique_site_name'),
),
migrations.AddConstraint(
model_name='powerport',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_powerport_unique_device_name'),
),
migrations.AddConstraint(
model_name='powerporttemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_powerporttemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='powerporttemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_powerporttemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='rack',
constraint=models.UniqueConstraint(fields=('location', 'name'), name='dcim_rack_unique_location_name'),
),
migrations.AddConstraint(
model_name='rack',
constraint=models.UniqueConstraint(fields=('location', 'facility_id'), name='dcim_rack_unique_location_facility_id'),
),
migrations.AddConstraint(
model_name='rearport',
constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_rearport_unique_device_name'),
),
migrations.AddConstraint(
model_name='rearporttemplate',
constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_rearporttemplate_unique_device_type_name'),
),
migrations.AddConstraint(
model_name='rearporttemplate',
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_rearporttemplate_unique_module_type_name'),
),
migrations.AddConstraint(
model_name='region',
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_region_name', violation_error_message='A top-level region with this name already exists.'),
),
migrations.AddConstraint(
model_name='region',
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_region_slug', violation_error_message='A top-level region with this slug already exists.'),
),
migrations.AddConstraint(
model_name='sitegroup',
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_sitegroup_name', violation_error_message='A top-level site group with this name already exists.'),
),
migrations.AddConstraint(
model_name='sitegroup',
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_sitegroup_slug', violation_error_message='A top-level site group with this slug already exists.'),
),
]

View File

@ -269,7 +269,7 @@ class CableTermination(models.Model):
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('termination_type', 'termination_id'), fields=('termination_type', 'termination_id'),
name='dcim_cable_termination_unique_termination' name='%(app_label)s_%(class)s_unique_termination'
), ),
) )

View File

@ -61,6 +61,13 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
class Meta: class Meta:
abstract = True abstract = True
ordering = ('device_type', '_name')
constraints = (
models.UniqueConstraint(
fields=('device_type', 'name'),
name='%(app_label)s_%(class)s_unique_device_type_name'
),
)
def __str__(self): def __str__(self):
if self.label: if self.label:
@ -100,6 +107,17 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
class Meta: class Meta:
abstract = True abstract = True
ordering = ('device_type', 'module_type', '_name')
constraints = (
models.UniqueConstraint(
fields=('device_type', 'name'),
name='%(app_label)s_%(class)s_unique_device_type_name'
),
models.UniqueConstraint(
fields=('module_type', 'name'),
name='%(app_label)s_%(class)s_unique_module_type_name'
),
)
def to_objectchange(self, action): def to_objectchange(self, action):
objectchange = super().to_objectchange(action) objectchange = super().to_objectchange(action)
@ -145,13 +163,6 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
component_model = ConsolePort component_model = ConsolePort
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
@ -181,13 +192,6 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
component_model = ConsoleServerPort component_model = ConsoleServerPort
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
@ -229,13 +233,6 @@ class PowerPortTemplate(ModularComponentTemplateModel):
component_model = PowerPort component_model = PowerPort
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
@ -291,13 +288,6 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
component_model = PowerOutlet component_model = PowerOutlet
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def clean(self): def clean(self):
super().clean() super().clean()
@ -372,13 +362,6 @@ class InterfaceTemplate(ModularComponentTemplateModel):
component_model = Interface component_model = Interface
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
@ -428,12 +411,20 @@ class FrontPortTemplate(ModularComponentTemplateModel):
component_model = FrontPort component_model = FrontPort
class Meta: class Meta(ModularComponentTemplateModel.Meta):
ordering = ('device_type', 'module_type', '_name') constraints = (
unique_together = ( models.UniqueConstraint(
('device_type', 'name'), fields=('device_type', 'name'),
('module_type', 'name'), name='%(app_label)s_%(class)s_unique_device_type_name'
('rear_port', 'rear_port_position'), ),
models.UniqueConstraint(
fields=('module_type', 'name'),
name='%(app_label)s_%(class)s_unique_module_type_name'
),
models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='%(app_label)s_%(class)s_unique_rear_port_position'
),
) )
def clean(self): def clean(self):
@ -507,13 +498,6 @@ class RearPortTemplate(ModularComponentTemplateModel):
component_model = RearPort component_model = RearPort
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
@ -547,10 +531,6 @@ class ModuleBayTemplate(ComponentTemplateModel):
component_model = ModuleBay component_model = ModuleBay
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def instantiate(self, device): def instantiate(self, device):
return self.component_model( return self.component_model(
device=device, device=device,
@ -574,10 +554,6 @@ class DeviceBayTemplate(ComponentTemplateModel):
""" """
component_model = DeviceBay component_model = DeviceBay
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def instantiate(self, device): def instantiate(self, device):
return self.component_model( return self.component_model(
device=device, device=device,
@ -653,7 +629,12 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
class Meta: class Meta:
ordering = ('device_type__id', 'parent__id', '_name') ordering = ('device_type__id', 'parent__id', '_name')
unique_together = ('device_type', 'parent', 'name') constraints = (
models.UniqueConstraint(
fields=('device_type', 'parent', 'name'),
name='%(app_label)s_%(class)s_unique_device_type_parent_name'
),
)
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None

View File

@ -69,6 +69,13 @@ class ComponentModel(NetBoxModel):
class Meta: class Meta:
abstract = True abstract = True
ordering = ('device', '_name')
constraints = (
models.UniqueConstraint(
fields=('device', 'name'),
name='%(app_label)s_%(class)s_unique_device_name'
),
)
def __str__(self): def __str__(self):
if self.label: if self.label:
@ -99,7 +106,7 @@ class ModularComponentModel(ComponentModel):
object_id_field='component_id' object_id_field='component_id'
) )
class Meta: class Meta(ComponentModel.Meta):
abstract = True abstract = True
@ -265,10 +272,6 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
clone_fields = ('device', 'module', 'type', 'speed') clone_fields = ('device', 'module', 'type', 'speed')
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:consoleport', kwargs={'pk': self.pk}) return reverse('dcim:consoleport', kwargs={'pk': self.pk})
@ -292,10 +295,6 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
clone_fields = ('device', 'module', 'type', 'speed') clone_fields = ('device', 'module', 'type', 'speed')
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:consoleserverport', kwargs={'pk': self.pk}) return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
@ -329,10 +328,6 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw') clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:powerport', kwargs={'pk': self.pk}) return reverse('dcim:powerport', kwargs={'pk': self.pk})
@ -443,10 +438,6 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg') clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:poweroutlet', kwargs={'pk': self.pk}) return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
@ -677,9 +668,8 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf',
) )
class Meta: class Meta(ModularComponentModel.Meta):
ordering = ('device', CollateAsChar('_name')) ordering = ('device', CollateAsChar('_name'))
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:interface', kwargs={'pk': self.pk}) return reverse('dcim:interface', kwargs={'pk': self.pk})
@ -895,11 +885,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
clone_fields = ('device', 'type', 'color') clone_fields = ('device', 'type', 'color')
class Meta: class Meta(ModularComponentModel.Meta):
ordering = ('device', '_name') constraints = (
unique_together = ( models.UniqueConstraint(
('device', 'name'), fields=('device', 'name'),
('rear_port', 'rear_port_position'), name='%(app_label)s_%(class)s_unique_device_name'
),
models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='%(app_label)s_%(class)s_unique_rear_port_position'
),
) )
def get_absolute_url(self): def get_absolute_url(self):
@ -944,10 +939,6 @@ class RearPort(ModularComponentModel, CabledObjectModel):
) )
clone_fields = ('device', 'type', 'color', 'positions') clone_fields = ('device', 'type', 'color', 'positions')
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:rearport', kwargs={'pk': self.pk}) return reverse('dcim:rearport', kwargs={'pk': self.pk})
@ -980,10 +971,6 @@ class ModuleBay(ComponentModel):
clone_fields = ('device',) clone_fields = ('device',)
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:modulebay', kwargs={'pk': self.pk}) return reverse('dcim:modulebay', kwargs={'pk': self.pk})
@ -1002,10 +989,6 @@ class DeviceBay(ComponentModel):
clone_fields = ('device',) clone_fields = ('device',)
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:devicebay', kwargs={'pk': self.pk}) return reverse('dcim:devicebay', kwargs={'pk': self.pk})
@ -1141,7 +1124,12 @@ class InventoryItem(MPTTModel, ComponentModel):
class Meta: class Meta:
ordering = ('device__id', 'parent__id', '_name') ordering = ('device__id', 'parent__id', '_name')
unique_together = ('device', 'parent', 'name') constraints = (
models.UniqueConstraint(
fields=('device', 'parent', 'name'),
name='%(app_label)s_%(class)s_unique_device_parent_name'
),
)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk}) return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})

View File

@ -143,10 +143,16 @@ class DeviceType(NetBoxModel):
class Meta: class Meta:
ordering = ['manufacturer', 'model'] ordering = ['manufacturer', 'model']
unique_together = [ constraints = (
['manufacturer', 'model'], models.UniqueConstraint(
['manufacturer', 'slug'], fields=('manufacturer', 'model'),
] name='%(app_label)s_%(class)s_unique_manufacturer_model'
),
models.UniqueConstraint(
fields=('manufacturer', 'slug'),
name='%(app_label)s_%(class)s_unique_manufacturer_slug'
),
)
def __str__(self): def __str__(self):
return self.model return self.model
@ -341,8 +347,11 @@ class ModuleType(NetBoxModel):
class Meta: class Meta:
ordering = ('manufacturer', 'model') ordering = ('manufacturer', 'model')
unique_together = ( constraints = (
('manufacturer', 'model'), models.UniqueConstraint(
fields=('manufacturer', 'model'),
name='%(app_label)s_%(class)s_unique_manufacturer_model'
),
) )
def __str__(self): def __str__(self):
@ -651,10 +660,25 @@ class Device(NetBoxModel, ConfigContextModel):
class Meta: class Meta:
ordering = ('_name', 'pk') # Name may be null ordering = ('_name', 'pk') # Name may be null
unique_together = ( constraints = (
('site', 'tenant', 'name'), # See validate_unique below models.UniqueConstraint(
('rack', 'position', 'face'), fields=('name', 'site', 'tenant'),
('virtual_chassis', 'vc_position'), name='%(app_label)s_%(class)s_unique_name_site_tenant'
),
models.UniqueConstraint(
fields=('name', 'site'),
name='%(app_label)s_%(class)s_unique_name_site',
condition=Q(tenant__isnull=True),
violation_error_message="Device name must be unique per site."
),
models.UniqueConstraint(
fields=('rack', 'position', 'face'),
name='%(app_label)s_%(class)s_unique_rack_position_face'
),
models.UniqueConstraint(
fields=('virtual_chassis', 'vc_position'),
name='%(app_label)s_%(class)s_unique_virtual_chassis_vc_position'
),
) )
def __str__(self): def __str__(self):
@ -679,23 +703,6 @@ class Device(NetBoxModel, ConfigContextModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk]) return reverse('dcim:device', args=[self.pk])
def validate_unique(self, exclude=None):
# Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
# of the uniqueness constraint without manual intervention.
if self.name and hasattr(self, 'site') and self.tenant is None:
if Device.objects.exclude(pk=self.pk).filter(
name=self.name,
site=self.site,
tenant__isnull=True
):
raise ValidationError({
'name': 'A device with this name already exists.'
})
super().validate_unique(exclude)
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -50,7 +50,12 @@ class PowerPanel(NetBoxModel):
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']
unique_together = ['site', 'name'] constraints = (
models.UniqueConstraint(
fields=('site', 'name'),
name='%(app_label)s_%(class)s_unique_site_name'
),
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -138,7 +143,12 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
class Meta: class Meta:
ordering = ['power_panel', 'name'] ordering = ['power_panel', 'name']
unique_together = ['power_panel', 'name'] constraints = (
models.UniqueConstraint(
fields=('power_panel', 'name'),
name='%(app_label)s_%(class)s_unique_power_panel_name'
),
)
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -3,12 +3,11 @@ import decimal
from django.apps import apps from django.apps import apps
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Sum from django.db.models import Count
from django.urls import reverse from django.urls import reverse
from dcim.choices import * from dcim.choices import *
@ -18,7 +17,7 @@ from netbox.models import OrganizationalModel, NetBoxModel
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange from utilities.utils import array_to_string, drange
from .device_components import PowerOutlet, PowerPort from .device_components import PowerPort
from .devices import Device from .devices import Device
from .power import PowerFeed from .power import PowerFeed
@ -191,10 +190,16 @@ class Rack(NetBoxModel):
class Meta: class Meta:
ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique
unique_together = ( constraints = (
# Name and facility_id must be unique *only* within a Location # Name and facility_id must be unique *only* within a Location
('location', 'name'), models.UniqueConstraint(
('location', 'facility_id'), fields=('location', 'name'),
name='%(app_label)s_%(class)s_unique_location_name'
),
models.UniqueConstraint(
fields=('location', 'facility_id'),
name='%(app_label)s_%(class)s_unique_location_facility_id'
),
) )
def __str__(self): def __str__(self):

View File

@ -62,38 +62,26 @@ class Region(NestedGroupModel):
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('parent', 'name'), fields=('parent', 'name'),
name='dcim_region_parent_name' name='%(app_label)s_%(class)s_parent_name'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('name',), fields=('name',),
name='dcim_region_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent=None) condition=Q(parent__isnull=True),
violation_error_message="A top-level region with this name already exists."
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('parent', 'slug'), fields=('parent', 'slug'),
name='dcim_region_parent_slug' name='%(app_label)s_%(class)s_parent_slug'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('slug',), fields=('slug',),
name='dcim_region_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent=None) condition=Q(parent__isnull=True),
violation_error_message="A top-level region with this slug already exists."
), ),
) )
def validate_unique(self, exclude=None):
if self.parent is None:
regions = Region.objects.exclude(pk=self.pk)
if regions.filter(name=self.name, parent__isnull=True).exists():
raise ValidationError({
'name': 'A region with this name already exists.'
})
if regions.filter(slug=self.slug, parent__isnull=True).exists():
raise ValidationError({
'name': 'A region with this slug already exists.'
})
super().validate_unique(exclude=exclude)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:region', args=[self.pk]) return reverse('dcim:region', args=[self.pk])
@ -148,38 +136,26 @@ class SiteGroup(NestedGroupModel):
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('parent', 'name'), fields=('parent', 'name'),
name='dcim_sitegroup_parent_name' name='%(app_label)s_%(class)s_parent_name'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('name',), fields=('name',),
name='dcim_sitegroup_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent=None) condition=Q(parent__isnull=True),
violation_error_message="A top-level site group with this name already exists."
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('parent', 'slug'), fields=('parent', 'slug'),
name='dcim_sitegroup_parent_slug' name='%(app_label)s_%(class)s_parent_slug'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('slug',), fields=('slug',),
name='dcim_sitegroup_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent=None) condition=Q(parent__isnull=True),
violation_error_message="A top-level site group with this slug already exists."
), ),
) )
def validate_unique(self, exclude=None):
if self.parent is None:
site_groups = SiteGroup.objects.exclude(pk=self.pk)
if site_groups.filter(name=self.name, parent__isnull=True).exists():
raise ValidationError({
'name': 'A site group with this name already exists.'
})
if site_groups.filter(slug=self.slug, parent__isnull=True).exists():
raise ValidationError({
'name': 'A site group with this slug already exists.'
})
super().validate_unique(exclude=exclude)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:sitegroup', args=[self.pk]) return reverse('dcim:sitegroup', args=[self.pk])
@ -379,38 +355,26 @@ class Location(NestedGroupModel):
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('site', 'parent', 'name'), fields=('site', 'parent', 'name'),
name='dcim_location_parent_name' name='%(app_label)s_%(class)s_parent_name'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('site', 'name'), fields=('site', 'name'),
name='dcim_location_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent=None) condition=Q(parent__isnull=True),
violation_error_message="A location with this name already exists within the specified site."
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('site', 'parent', 'slug'), fields=('site', 'parent', 'slug'),
name='dcim_location_parent_slug' name='%(app_label)s_%(class)s_parent_slug'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('site', 'slug'), fields=('site', 'slug'),
name='dcim_location_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent=None) condition=Q(parent__isnull=True),
violation_error_message="A location with this slug already exists within the specified site."
), ),
) )
def validate_unique(self, exclude=None):
if self.parent is None:
locations = Location.objects.exclude(pk=self.pk)
if locations.filter(name=self.name, site=self.site, parent__isnull=True).exists():
raise ValidationError({
"name": f"A location with this name in site {self.site} already exists."
})
if locations.filter(slug=self.slug, site=self.site, parent__isnull=True).exists():
raise ValidationError({
"name": f"A location with this slug in site {self.site} already exists."
})
super().validate_unique(exclude=exclude)
@classmethod @classmethod
def get_prerequisite_models(cls): def get_prerequisite_models(cls):
return [Site, ] return [Site, ]

View File

@ -384,7 +384,7 @@ class DeviceTestCase(TestCase):
site=self.site, site=self.site,
device_type=self.device_type, device_type=self.device_type,
device_role=self.device_role, device_role=self.device_role,
name='' name=None
) )
device1.save() device1.save()
@ -392,12 +392,12 @@ class DeviceTestCase(TestCase):
site=device1.site, site=device1.site,
device_type=device1.device_type, device_type=device1.device_type,
device_role=device1.device_role, device_role=device1.device_role,
name='' name=None
) )
device2.full_clean() device2.full_clean()
device2.save() device2.save()
self.assertEqual(Device.objects.filter(name='').count(), 2) self.assertEqual(Device.objects.filter(name__isnull=True).count(), 2)
def test_device_duplicate_names(self): def test_device_duplicate_names(self):

View File

@ -0,0 +1,27 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0077_customlink_extend_text_and_url'),
]
operations = [
migrations.AlterUniqueTogether(
name='exporttemplate',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='webhook',
unique_together=set(),
),
migrations.AddConstraint(
model_name='exporttemplate',
constraint=models.UniqueConstraint(fields=('content_type', 'name'), name='extras_exporttemplate_unique_content_type_name'),
),
migrations.AddConstraint(
model_name='webhook',
constraint=models.UniqueConstraint(fields=('payload_url', 'type_create', 'type_update', 'type_delete'), name='extras_webhook_unique_payload_url_types'),
),
]

View File

@ -131,7 +131,12 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class Meta: class Meta:
ordering = ('name',) ordering = ('name',)
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',) constraints = (
models.UniqueConstraint(
fields=('payload_url', 'type_create', 'type_update', 'type_delete'),
name='%(app_label)s_%(class)s_unique_payload_url_types'
),
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -297,9 +302,12 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class Meta: class Meta:
ordering = ['content_type', 'name'] ordering = ['content_type', 'name']
unique_together = [ constraints = (
['content_type', 'name'] models.UniqueConstraint(
] fields=('content_type', 'name'),
name='%(app_label)s_%(class)s_unique_content_type_name'
),
)
def __str__(self): def __str__(self):
return f"{self.content_type}: {self.name}" return f"{self.content_type}: {self.name}"

View File

@ -123,7 +123,7 @@ class FHRPGroupSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = [ fields = [
'id', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses',
'tags', 'custom_fields', 'created', 'last_updated', 'tags', 'custom_fields', 'created', 'last_updated',
] ]

View File

@ -653,13 +653,14 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ['id', 'group_id', 'auth_key'] fields = ['id', 'group_id', 'name', 'auth_key']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(description__icontains=value) Q(description__icontains=value) |
Q(name__icontains=value)
) )
def filter_related_ip(self, queryset, name, value): def filter_related_ip(self, queryset, name, value):

View File

@ -321,6 +321,10 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label='Authentication key' label='Authentication key'
) )
name = forms.CharField(
max_length=100,
required=False
)
description = forms.CharField( description = forms.CharField(
max_length=200, max_length=200,
required=False required=False
@ -328,10 +332,10 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('protocol', 'group_id', 'description')), (None, ('protocol', 'group_id', 'name', 'description')),
('Authentication', ('auth_type', 'auth_key')), ('Authentication', ('auth_type', 'auth_key')),
) )
nullable_fields = ('auth_type', 'auth_key', 'description') nullable_fields = ('auth_type', 'auth_key', 'name', 'description')
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):

View File

@ -326,7 +326,7 @@ class FHRPGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'description') fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description')
class VLANGroupCSVForm(NetBoxModelCSVForm): class VLANGroupCSVForm(NetBoxModelCSVForm):

View File

@ -335,9 +335,12 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Attributes', ('protocol', 'group_id')), ('Attributes', ('name', 'protocol', 'group_id')),
('Authentication', ('auth_type', 'auth_key')), ('Authentication', ('auth_type', 'auth_key')),
) )
name = forms.CharField(
required=False
)
protocol = MultipleChoiceField( protocol = MultipleChoiceField(
choices=FHRPGroupProtocolChoices, choices=FHRPGroupProtocolChoices,
required=False required=False

View File

@ -527,7 +527,7 @@ class FHRPGroupForm(NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
('FHRP Group', ('protocol', 'group_id', 'description', 'tags')), ('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')),
('Authentication', ('auth_type', 'auth_key')), ('Authentication', ('auth_type', 'auth_key')),
('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status')) ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status'))
) )
@ -535,7 +535,7 @@ class FHRPGroupForm(NetBoxModelForm):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ( fields = (
'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags', 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags',
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-09-20 23:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0060_alter_l2vpn_slug'),
]
operations = [
migrations.AddField(
model_name='fhrpgroup',
name='name',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -0,0 +1,43 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0061_fhrpgroup_name'),
]
operations = [
migrations.AlterUniqueTogether(
name='fhrpgroupassignment',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='vlan',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='vlangroup',
unique_together=set(),
),
migrations.AddConstraint(
model_name='fhrpgroupassignment',
constraint=models.UniqueConstraint(fields=('interface_type', 'interface_id', 'group'), name='ipam_fhrpgroupassignment_unique_interface_group'),
),
migrations.AddConstraint(
model_name='vlan',
constraint=models.UniqueConstraint(fields=('group', 'vid'), name='ipam_vlan_unique_group_vid'),
),
migrations.AddConstraint(
model_name='vlan',
constraint=models.UniqueConstraint(fields=('group', 'name'), name='ipam_vlan_unique_group_name'),
),
migrations.AddConstraint(
model_name='vlangroup',
constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name'), name='ipam_vlangroup_unique_scope_name'),
),
migrations.AddConstraint(
model_name='vlangroup',
constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'slug'), name='ipam_vlangroup_unique_scope_slug'),
),
]

View File

@ -22,6 +22,10 @@ class FHRPGroup(NetBoxModel):
group_id = models.PositiveSmallIntegerField( group_id = models.PositiveSmallIntegerField(
verbose_name='Group ID' verbose_name='Group ID'
) )
name = models.CharField(
max_length=100,
blank=True
)
protocol = models.CharField( protocol = models.CharField(
max_length=50, max_length=50,
choices=FHRPGroupProtocolChoices choices=FHRPGroupProtocolChoices
@ -55,7 +59,11 @@ class FHRPGroup(NetBoxModel):
verbose_name = 'FHRP group' verbose_name = 'FHRP group'
def __str__(self): def __str__(self):
name = f'{self.get_protocol_display()}: {self.group_id}' name = ''
if self.name:
name = f'{self.name} '
name += f'{self.get_protocol_display()}: {self.group_id}'
# Append the first assigned IP addresses (if any) to serve as an additional identifier # Append the first assigned IP addresses (if any) to serve as an additional identifier
if self.pk: if self.pk:
@ -94,7 +102,12 @@ class FHRPGroupAssignment(WebhooksMixin, ChangeLoggedModel):
class Meta: class Meta:
ordering = ('-priority', 'pk') ordering = ('-priority', 'pk')
unique_together = ('interface_type', 'interface_id', 'group') constraints = (
models.UniqueConstraint(
fields=('interface_type', 'interface_id', 'group'),
name='%(app_label)s_%(class)s_unique_interface_group'
),
)
verbose_name = 'FHRP group assignment' verbose_name = 'FHRP group assignment'
def __str__(self): def __str__(self):

View File

@ -70,10 +70,16 @@ class VLANGroup(OrganizationalModel):
class Meta: class Meta:
ordering = ('name', 'pk') # Name may be non-unique ordering = ('name', 'pk') # Name may be non-unique
unique_together = [ constraints = (
['scope_type', 'scope_id', 'name'], models.UniqueConstraint(
['scope_type', 'scope_id', 'slug'], fields=('scope_type', 'scope_id', 'name'),
] name='%(app_label)s_%(class)s_unique_scope_name'
),
models.UniqueConstraint(
fields=('scope_type', 'scope_id', 'slug'),
name='%(app_label)s_%(class)s_unique_scope_slug'
),
)
verbose_name = 'VLAN group' verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups' verbose_name_plural = 'VLAN groups'
@ -189,10 +195,16 @@ class VLAN(NetBoxModel):
class Meta: class Meta:
ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
unique_together = [ constraints = (
['group', 'vid'], models.UniqueConstraint(
['group', 'name'], fields=('group', 'vid'),
] name='%(app_label)s_%(class)s_unique_group_vid'
),
models.UniqueConstraint(
fields=('group', 'name'),
name='%(app_label)s_%(class)s_unique_group_name'
),
)
verbose_name = 'VLAN' verbose_name = 'VLAN'
verbose_name_plural = 'VLANs' verbose_name_plural = 'VLANs'

View File

@ -36,10 +36,12 @@ class FHRPGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = FHRPGroup model = FHRPGroup
fields = ( fields = (
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count', 'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'ip_addresses',
'tags', 'created', 'last_updated', 'member_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'group_id', 'protocol', 'name', 'auth_type', 'description', 'ip_addresses', 'member_count',
) )
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count')
class FHRPGroupAssignmentTable(NetBoxTable): class FHRPGroupAssignmentTable(NetBoxTable):

View File

@ -552,6 +552,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase):
'group_id': 200, 'group_id': 200,
'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
'auth_key': 'foobarbaz999', 'auth_key': 'foobarbaz999',
'name': 'foobar-999',
'description': 'New description', 'description': 'New description',
} }

View File

@ -932,7 +932,7 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
fhrp_groups = ( fhrp_groups = (
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foo123'), FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foo123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='bar456'), FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='bar456', name='bar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30), FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
) )
FHRPGroup.objects.bulk_create(fhrp_groups) FHRPGroup.objects.bulk_create(fhrp_groups)
@ -956,6 +956,10 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'auth_key': ['foo123', 'bar456']} params = {'auth_key': ['foo123', 'bar456']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['bar123', ]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_related_ip(self): def test_related_ip(self):
# Create some regular IPs to query for related IPs # Create some regular IPs to query for related IPs
ipaddresses = ( ipaddresses = (

View File

@ -524,6 +524,7 @@ class FHRPGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
'auth_key': 'abc123def456', 'auth_key': 'abc123def456',
'description': 'Blah blah blah', 'description': 'Blah blah blah',
'name': 'test123 name',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }

View File

@ -26,6 +26,10 @@
<td>Group ID</td> <td>Group ID</td>
<td>{{ object.group_id }}</td> <td>{{ object.group_id }}</td>
</tr> </tr>
<tr>
<td>Name</td>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr> <tr>
<td>Description</td> <td>Description</td>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>

View File

@ -8,6 +8,7 @@
</div> </div>
{% render_field form.protocol %} {% render_field form.protocol %}
{% render_field form.group_id %} {% render_field form.group_id %}
{% render_field form.name %}
{% render_field form.description %} {% render_field form.description %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>

View File

@ -0,0 +1,35 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0007_contact_link'),
]
operations = [
migrations.AlterUniqueTogether(
name='contact',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='contactassignment',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='contactgroup',
unique_together=set(),
),
migrations.AddConstraint(
model_name='contact',
constraint=models.UniqueConstraint(fields=('group', 'name'), name='tenancy_contact_unique_group_name'),
),
migrations.AddConstraint(
model_name='contactassignment',
constraint=models.UniqueConstraint(fields=('content_type', 'object_id', 'contact', 'role'), name='tenancy_contactassignment_unique_object_contact_role'),
),
migrations.AddConstraint(
model_name='contactgroup',
constraint=models.UniqueConstraint(fields=('parent', 'name'), name='tenancy_contactgroup_unique_parent_name'),
),
]

View File

@ -41,8 +41,11 @@ class ContactGroup(NestedGroupModel):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ( constraints = (
('parent', 'name') models.UniqueConstraint(
fields=('parent', 'name'),
name='%(app_label)s_%(class)s_unique_parent_name'
),
) )
def get_absolute_url(self): def get_absolute_url(self):
@ -118,8 +121,11 @@ class Contact(NetBoxModel):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ( constraints = (
('group', 'name') models.UniqueConstraint(
fields=('group', 'name'),
name='%(app_label)s_%(class)s_unique_group_name'
),
) )
def __str__(self): def __str__(self):
@ -159,7 +165,12 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
class Meta: class Meta:
ordering = ('priority', 'contact') ordering = ('priority', 'contact')
unique_together = ('content_type', 'object_id', 'contact', 'role', 'priority') constraints = (
models.UniqueConstraint(
fields=('content_type', 'object_id', 'contact', 'role'),
name='%(app_label)s_%(class)s_unique_object_contact_role'
),
)
def __str__(self): def __str__(self):
if self.priority: if self.priority:

View File

@ -0,0 +1,43 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0032_virtualmachine_update_sites'),
]
operations = [
migrations.AlterUniqueTogether(
name='cluster',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='virtualmachine',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='vminterface',
unique_together=set(),
),
migrations.AddConstraint(
model_name='cluster',
constraint=models.UniqueConstraint(fields=('group', 'name'), name='virtualization_cluster_unique_group_name'),
),
migrations.AddConstraint(
model_name='cluster',
constraint=models.UniqueConstraint(fields=('site', 'name'), name='virtualization_cluster_unique_site_name'),
),
migrations.AddConstraint(
model_name='virtualmachine',
constraint=models.UniqueConstraint(fields=('name', 'cluster', 'tenant'), name='virtualization_virtualmachine_unique_name_cluster_tenant'),
),
migrations.AddConstraint(
model_name='virtualmachine',
constraint=models.UniqueConstraint(condition=models.Q(('tenant__isnull', True)), fields=('name', 'cluster'), name='virtualization_virtualmachine_unique_name_cluster', violation_error_message='Virtual machine name must be unique per site.'),
),
migrations.AddConstraint(
model_name='vminterface',
constraint=models.UniqueConstraint(fields=('virtual_machine', 'name'), name='virtualization_vminterface_unique_virtual_machine_name'),
),
]

View File

@ -2,6 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from dcim.models import BaseInterface, Device from dcim.models import BaseInterface, Device
@ -159,9 +160,15 @@ class Cluster(NetBoxModel):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ( constraints = (
('group', 'name'), models.UniqueConstraint(
('site', 'name'), fields=('group', 'name'),
name='%(app_label)s_%(class)s_unique_group_name'
),
models.UniqueConstraint(
fields=('site', 'name'),
name='%(app_label)s_%(class)s_unique_site_name'
),
) )
def __str__(self): def __str__(self):
@ -309,9 +316,18 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
class Meta: class Meta:
ordering = ('_name', 'pk') # Name may be non-unique ordering = ('_name', 'pk') # Name may be non-unique
unique_together = [ constraints = (
['cluster', 'tenant', 'name'] models.UniqueConstraint(
] fields=('name', 'cluster', 'tenant'),
name='%(app_label)s_%(class)s_unique_name_cluster_tenant'
),
models.UniqueConstraint(
fields=('name', 'cluster'),
name='%(app_label)s_%(class)s_unique_name_cluster',
condition=Q(tenant__isnull=True),
violation_error_message="Virtual machine name must be unique per site."
),
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -323,20 +339,6 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('virtualization:virtualmachine', args=[self.pk]) return reverse('virtualization:virtualmachine', args=[self.pk])
def validate_unique(self, exclude=None):
# Check for a duplicate name on a VM assigned to the same Cluster and no Tenant. This is necessary
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
# of the uniqueness constraint without manual intervention.
if self.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter(
name=self.name, cluster=self.cluster, tenant__isnull=True
):
raise ValidationError({
'name': 'A virtual machine with this name already exists in the assigned cluster.'
})
super().validate_unique(exclude)
def clean(self): def clean(self):
super().clean() super().clean()
@ -465,9 +467,14 @@ class VMInterface(NetBoxModel, BaseInterface):
) )
class Meta: class Meta:
verbose_name = 'interface'
ordering = ('virtual_machine', CollateAsChar('_name')) ordering = ('virtual_machine', CollateAsChar('_name'))
unique_together = ('virtual_machine', 'name') constraints = (
models.UniqueConstraint(
fields=('virtual_machine', 'name'),
name='%(app_label)s_%(class)s_unique_virtual_machine_name'
),
)
verbose_name = 'interface'
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -0,0 +1,27 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wireless', '0005_wirelesslink_interface_types'),
]
operations = [
migrations.AlterUniqueTogether(
name='wirelesslangroup',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='wirelesslink',
unique_together=set(),
),
migrations.AddConstraint(
model_name='wirelesslangroup',
constraint=models.UniqueConstraint(fields=('parent', 'name'), name='wireless_wirelesslangroup_unique_parent_name'),
),
migrations.AddConstraint(
model_name='wirelesslink',
constraint=models.UniqueConstraint(fields=('interface_a', 'interface_b'), name='wireless_wirelesslink_unique_interfaces'),
),
]

View File

@ -69,8 +69,11 @@ class WirelessLANGroup(NestedGroupModel):
class Meta: class Meta:
ordering = ('name', 'pk') ordering = ('name', 'pk')
unique_together = ( constraints = (
('parent', 'name') models.UniqueConstraint(
fields=('parent', 'name'),
name='%(app_label)s_%(class)s_unique_parent_name'
),
) )
verbose_name = 'Wireless LAN Group' verbose_name = 'Wireless LAN Group'
@ -195,7 +198,12 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel):
class Meta: class Meta:
ordering = ['pk'] ordering = ['pk']
unique_together = ('interface_a', 'interface_b') constraints = (
models.UniqueConstraint(
fields=('interface_a', 'interface_b'),
name='%(app_label)s_%(class)s_unique_interfaces'
),
)
def __str__(self): def __str__(self):
return f'#{self.pk}' return f'#{self.pk}'