diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9174085b8..5250045c5 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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'] # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cd728cd19..831f7be9e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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(), diff --git a/netbox/dcim/migrations/0107_component_labels.py b/netbox/dcim/migrations/0107_component_labels.py new file mode 100644 index 000000000..8e5ab8156 --- /dev/null +++ b/netbox/dcim/migrations/0107_component_labels.py @@ -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), + ), + ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index e412a602e..904352196 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -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, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 702455c7e..a626c055f 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -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): diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 29e741560..aadc2cbfc 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -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) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index cc7b8aa60..eb1e47a66 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -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', diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 9d94e0639..5714c8940 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -58,6 +58,10 @@