Merge pull request #4723 from jsenecal/4615_interface_label

Closes: #4615 Physical labels
This commit is contained in:
Jeremy Stretch 2020-06-11 13:03:46 -04:00 committed by GitHub
commit 9b48a26aef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 275 additions and 109 deletions

View File

@ -249,7 +249,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
fields = ['id', 'device_type', 'name', 'type']
fields = ['id', 'device_type', 'name', 'label', 'type']
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
@ -262,7 +262,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name', 'type']
fields = ['id', 'device_type', 'name', 'label', 'type']
class PowerPortTemplateSerializer(ValidatedModelSerializer):
@ -275,7 +275,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
@ -296,7 +296,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg']
class InterfaceTemplateSerializer(ValidatedModelSerializer):
@ -305,7 +305,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only']
class RearPortTemplateSerializer(ValidatedModelSerializer):
@ -332,7 +332,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBayTemplate
fields = ['id', 'device_type', 'name']
fields = ['id', 'device_type', 'name', 'label']
#
@ -447,7 +447,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
class Meta:
model = ConsoleServerPort
fields = [
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'connection_status', 'cable', 'tags',
]
@ -465,7 +465,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = ConsolePort
fields = [
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'connection_status', 'cable', 'tags',
]
@ -495,7 +495,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = PowerOutlet
fields = [
'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'id', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags',
]
@ -513,7 +513,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = PowerPort
fields = [
'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'id', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags',
]
@ -537,7 +537,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'id', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
'tagged_vlans', 'tags', 'count_ipaddresses',
]
@ -604,7 +604,7 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
class Meta:
model = DeviceBay
fields = ['id', 'device', 'name', 'description', 'installed_device', 'tags']
fields = ['id', 'device', 'name', 'label', 'description', 'installed_device', 'tags']
#

View File

@ -22,10 +22,10 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField,
CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model,
JSONField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES,
BOOLEAN_WITH_BLANK_CHOICES, BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField,
CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
form_from_model, JSONField, LabeledComponentForm, SelectWithPK, SmallTextarea, SlugField, StaticSelect2,
StaticSelect2Multiple, TagFilterField,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
from .choices import *
@ -1037,20 +1037,17 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'name', 'type',
'device_type', 'name', 'label', 'type',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
class ConsolePortTemplateCreateForm(LabeledComponentForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all()
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect2()
@ -1077,20 +1074,17 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'name', 'type',
'device_type', 'name', 'label', 'type',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
class ConsoleServerPortTemplateCreateForm(LabeledComponentForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all()
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect2()
@ -1117,20 +1111,17 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
class PowerPortTemplateCreateForm(LabeledComponentForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all()
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(PowerPortTypeChoices),
required=False
@ -1177,7 +1168,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'name', 'type', 'power_port', 'feed_leg',
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
]
widgets = {
'device_type': forms.HiddenInput(),
@ -1194,13 +1185,10 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
)
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
class PowerOutletTemplateCreateForm(LabeledComponentForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all()
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices),
required=False
@ -1252,7 +1240,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'name', 'type', 'mgmt_only',
'device_type', 'name', 'label', 'type', 'mgmt_only',
]
widgets = {
'device_type': forms.HiddenInput(),
@ -1260,13 +1248,10 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
}
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
class InterfaceTemplateCreateForm(LabeledComponentForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all()
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=InterfaceTypeChoices,
widget=StaticSelect2()
@ -1509,7 +1494,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'name', 'type',
'device_type', 'name', 'label', 'type',
]
@ -1518,7 +1503,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'name', 'type',
'device_type', 'name', 'label', 'type',
]
@ -1527,7 +1512,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
]
@ -1541,7 +1526,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'name', 'type', 'power_port', 'feed_leg',
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
]
@ -1553,7 +1538,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'name', 'type', 'mgmt_only',
'device_type', 'name', 'label', 'type', 'mgmt_only',
]
@ -2196,14 +2181,11 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
# Bulk device component creation
#
class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
class DeviceBulkAddComponentForm(LabeledComponentForm):
pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput()
)
name_pattern = ExpandableNameField(
label='Name'
)
def clean_tags(self):
# Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we
@ -2234,20 +2216,17 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsolePort
fields = [
'device', 'name', 'type', 'description', 'tags',
'device', 'name', 'label', 'type', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
}
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
class ConsolePortCreateForm(LabeledComponentForm):
device = DynamicModelChoiceField(
queryset=Device.objects.prefetch_related('device_type__manufacturer')
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
@ -2327,13 +2306,10 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
}
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
class ConsoleServerPortCreateForm(LabeledComponentForm):
device = DynamicModelChoiceField(
queryset=Device.objects.prefetch_related('device_type__manufacturer')
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
@ -2427,13 +2403,10 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
}
class PowerPortCreateForm(BootstrapMixin, forms.Form):
class PowerPortCreateForm(LabeledComponentForm):
device = DynamicModelChoiceField(
queryset=Device.objects.prefetch_related('device_type__manufacturer')
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(PowerPortTypeChoices),
required=False,
@ -2536,13 +2509,10 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
)
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
class PowerOutletCreateForm(LabeledComponentForm):
device = DynamicModelChoiceField(
queryset=Device.objects.prefetch_related('device_type__manufacturer')
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices),
required=False,
@ -2726,7 +2696,7 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
class Meta:
model = Interface
fields = [
'device', 'name', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
'device', 'name', 'label', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
]
widgets = {
@ -2761,13 +2731,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
class InterfaceCreateForm(InterfaceCommonForm, LabeledComponentForm):
device = DynamicModelChoiceField(
queryset=Device.objects.prefetch_related('device_type__manufacturer')
)
name_pattern = ExpandableNameField(
label='Name'
)
type = forms.ChoiceField(
choices=InterfaceTypeChoices,
widget=StaticSelect2(),

View File

@ -0,0 +1,73 @@
# Generated by Django 3.0.7 on 2020-06-04 20:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0106_role_default_color'),
]
operations = [
migrations.AddField(
model_name='interface',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='interfacetemplate',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='consoleport',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='consoleporttemplate',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='consoleserverport',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='poweroutlet',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='powerport',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='powerporttemplate',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='devicebay',
name='label',
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name='devicebaytemplate',
name='label',
field=models.CharField(blank=True, max_length=64),
),
]

View File

@ -32,6 +32,11 @@ class ComponentTemplateModel(models.Model):
class Meta:
abstract = True
def __str__(self):
if self.label:
return f"{self.name} ({self.label})"
return self.name
def instantiate(self, device):
"""
Instantiate a new component on the specified Device.
@ -71,6 +76,11 @@ class ConsolePortTemplate(ComponentTemplateModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=ConsolePortTypeChoices,
@ -81,9 +91,6 @@ class ConsolePortTemplate(ComponentTemplateModel):
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def __str__(self):
return self.name
def instantiate(self, device):
return ConsolePort(
device=device,
@ -109,6 +116,11 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=ConsolePortTypeChoices,
@ -119,9 +131,6 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def __str__(self):
return self.name
def instantiate(self, device):
return ConsoleServerPort(
device=device,
@ -147,6 +156,11 @@ class PowerPortTemplate(ComponentTemplateModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=PowerPortTypeChoices,
@ -169,9 +183,6 @@ class PowerPortTemplate(ComponentTemplateModel):
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def __str__(self):
return self.name
def instantiate(self, device):
return PowerPort(
device=device,
@ -199,6 +210,11 @@ class PowerOutletTemplate(ComponentTemplateModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=PowerOutletTypeChoices,
@ -222,9 +238,6 @@ class PowerOutletTemplate(ComponentTemplateModel):
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def __str__(self):
return self.name
def clean(self):
# Validate power port assignment
@ -265,6 +278,11 @@ class InterfaceTemplate(ComponentTemplateModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=InterfaceTypeChoices
@ -278,9 +296,6 @@ class InterfaceTemplate(ComponentTemplateModel):
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def __str__(self):
return self.name
def instantiate(self, device):
return Interface(
device=device,
@ -420,14 +435,16 @@ class DeviceBayTemplate(ComponentTemplateModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def __str__(self):
return self.name
def instantiate(self, device):
return DeviceBay(
device=device,

View File

@ -47,6 +47,11 @@ class ComponentModel(models.Model):
class Meta:
abstract = True
def __str__(self):
if self.label:
return f"{self.name} ({self.label})"
return self.name
def to_objectchange(self, action):
# Annotate the parent Device/VM
try:
@ -234,6 +239,11 @@ class ConsolePort(CableTermination, ComponentModel):
name = models.CharField(
max_length=50
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
@ -264,9 +274,6 @@ class ConsolePort(CableTermination, ComponentModel):
ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
@ -301,6 +308,11 @@ class ConsoleServerPort(CableTermination, ComponentModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=ConsolePortTypeChoices,
@ -319,9 +331,6 @@ class ConsoleServerPort(CableTermination, ComponentModel):
ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
@ -356,6 +365,11 @@ class PowerPort(CableTermination, ComponentModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=PowerPortTypeChoices,
@ -400,9 +414,6 @@ class PowerPort(CableTermination, ComponentModel):
ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
@ -519,6 +530,11 @@ class PowerOutlet(CableTermination, ComponentModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
type = models.CharField(
max_length=50,
choices=PowerOutletTypeChoices,
@ -550,9 +566,6 @@ class PowerOutlet(CableTermination, ComponentModel):
ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
@ -608,6 +621,11 @@ class Interface(CableTermination, ComponentModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
_connected_interface = models.OneToOneField(
to='self',
on_delete=models.SET_NULL,
@ -688,9 +706,6 @@ class Interface(CableTermination, ComponentModel):
ordering = ('device', CollateAsChar('_name'))
unique_together = ('device', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('dcim:interface', kwargs={'pk': self.pk})
@ -997,6 +1012,11 @@ class DeviceBay(ComponentModel):
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
help_text="Physical label"
)
installed_device = models.OneToOneField(
to='dcim.Device',
on_delete=models.SET_NULL,
@ -1013,6 +1033,8 @@ class DeviceBay(ComponentModel):
unique_together = ('device', 'name')
def __str__(self):
if self.label:
return '{} - {} ({})'.format(self.device.name, self.name, self.label)
return '{} - {}'.format(self.device.name, self.name)
def get_absolute_url(self):

View File

@ -116,3 +116,45 @@ class DeviceTestCase(TestCase):
# Check that the initial value for the cluster group is set automatically when assigning the cluster
self.assertEqual(test.initial['cluster_group'], cluster.group.pk)
class LabelTestCase(TestCase):
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 2', slug='site-2')
manufacturer = Manufacturer.objects.create(name='Manufacturer 2', slug='manufacturer-2')
cls.device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=1
)
device_role = DeviceRole.objects.create(
name='Device Role 2', slug='device-role-2', color='ffff00'
)
cls.device = Device.objects.create(
name='Device 2', device_type=cls.device_type, device_role=device_role, site=site
)
def test_interface_label_count_valid(self):
"""Test that a `label` can be generated for each generated `name` from `name_pattern` on InterfaceCreateForm"""
interface_data = {
'device': self.device.pk,
'name_pattern': 'eth[0-9]',
'label_pattern': 'Interface[0-9]',
'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
}
form = InterfaceCreateForm(interface_data)
self.assertTrue(form.is_valid())
def test_interface_label_count_mismatch(self):
"""Test that a `label` cannot be generated for each generated `name` from `name_pattern` due to invalid `label_pattern` on InterfaceCreateForm"""
bad_interface_data = {
'device': self.device.pk,
'name_pattern': 'eth[0-9]',
'label_pattern': 'Interface[0-1]',
'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
}
form = InterfaceCreateForm(bad_interface_data)
self.assertFalse(form.is_valid())
self.assertIn('label_pattern', form.errors)

View File

@ -710,6 +710,8 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Interface Template [4-6]',
# Test that a label can be applied to each generated interface templates
'label_pattern': 'Interface Template Label [3-5]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mgmt_only': True,
}
@ -1010,6 +1012,8 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Console Port [4-6]',
# Test that a label can be applied to each generated console ports
'label_pattern': 'Serial[3-5]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port',
'tags': 'Alpha,Bravo,Charlie',

View File

@ -58,6 +58,10 @@
<td>Name</td>
<td>{{ interface.name }}</td>
</tr>
<tr>
<td>Label</td>
<td>{{ interface.label|placeholder }}</td>
</tr>
<tr>
<td>Type</td>
<td>{{ interface.get_type_display }}</td>

View File

@ -6,6 +6,7 @@
<div class="panel-heading"><strong>Interface</strong></div>
<div class="panel-body">
{% render_field form.name %}
{% render_field form.label %}
{% render_field form.type %}
{% render_field form.enabled %}
{% render_field form.lag %}

View File

@ -530,6 +530,8 @@ class ExpandableNameField(forms.CharField):
"""
def to_python(self, value):
if value is None:
return list()
if re.search(ALPHANUMERIC_EXPANSION_PATTERN, value):
return list(expand_alphanumeric_pattern(value))
return [value]
@ -802,6 +804,31 @@ class ImportForm(BootstrapMixin, forms.Form):
})
class LabeledComponentForm(BootstrapMixin, forms.Form):
"""
Base form for adding label pattern validation to `Create` forms
"""
name_pattern = ExpandableNameField(
label='Name'
)
label_pattern = ExpandableNameField(
label='Label',
required=False
)
def clean(self):
# Validate that the number of components being created from both the name_pattern and label_pattern are equal
name_pattern_count = len(self.cleaned_data['name_pattern'])
label_pattern_count = len(self.cleaned_data['label_pattern'])
if label_pattern_count and name_pattern_count != label_pattern_count:
raise forms.ValidationError({
'label_pattern': 'The provided name pattern will create {} components, however {} labels will '
'be generated. These counts must match.'.format(
name_pattern_count, label_pattern_count)
}, code='label_pattern_mismatch')
class TableConfigForm(BootstrapMixin, forms.Form):
"""
Form for configuring user's table preferences.

View File

@ -1088,10 +1088,13 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
new_components = []
data = deepcopy(request.POST)
for i, name in enumerate(form.cleaned_data['name_pattern']):
names = form.cleaned_data['name_pattern']
labels = form.cleaned_data.get('label_pattern')
for i, name in enumerate(names):
label = labels[i] if labels else None
# Initialize the individual component form
data['name'] = name
data['label'] = label
if hasattr(form, 'get_iterative_data'):
data.update(form.get_iterative_data(i))
component_form = self.model_form(data)
@ -1100,9 +1103,11 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
new_components.append(component_form)
else:
for field, errors in component_form.errors.as_data().items():
# Assign errors on the child form's name field to name_pattern on the parent form
# Assign errors on the child form's name/label field to name_pattern/label_pattern on the parent form
if field == 'name':
field = 'name_pattern'
elif field == 'label':
field = 'label_pattern'
for e in errors:
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
@ -1187,10 +1192,14 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
for obj in data['pk']:
names = data['name_pattern']
for name in names:
labels = data['label_pattern']
for i, name in enumerate(names):
label = labels[i] if labels else None
component_data = {
self.parent_field: obj.pk,
'name': name,
'label': label
}
component_data.update(data)
component_form = self.model_form(component_data)