diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 94cf51fcd..0e9d9763e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1032,7 +1032,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1046,11 +1046,27 @@ class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1072,7 +1088,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1086,11 +1102,27 @@ class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1112,7 +1144,7 @@ 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(), @@ -1126,6 +1158,10 @@ class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False @@ -1141,6 +1177,18 @@ class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): help_text="Allocated power draw (watts)" ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1172,7 +1220,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(), @@ -1196,6 +1244,10 @@ class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False @@ -1221,6 +1273,18 @@ class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): device_type=device_type ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1247,7 +1311,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(), @@ -1262,6 +1326,10 @@ class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2() @@ -1271,6 +1339,18 @@ class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): label='Management only' ) + def clean(self): + + # Validate that the number of ports 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 {} interfaces, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1504,7 +1584,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] @@ -1513,7 +1593,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] @@ -1522,7 +1602,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', ] @@ -1536,7 +1616,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', ] @@ -1548,7 +1628,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'name', 'type', 'mgmt_only', + 'device_type', 'name', 'label', 'type', 'mgmt_only', ] @@ -2199,12 +2279,28 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) def clean_tags(self): # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we # must first convert the list of tags to a string. return ','.join(self.cleaned_data.get('tags')) + def clean(self): + + # Validate that the number of ports 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 {} {}}, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, self.type, label_pattern_count) + }) + # # Console ports @@ -2229,7 +2325,7 @@ 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(), @@ -2243,6 +2339,10 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2256,6 +2356,18 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): required=False ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsolePortBulkCreateForm( form_from_model(ConsolePort, ['type', 'description', 'tags']), @@ -2329,6 +2441,10 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2342,6 +2458,18 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): required=False ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsoleServerPortBulkCreateForm( form_from_model(ConsoleServerPort, ['type', 'description', 'tags']), @@ -2429,6 +2557,10 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False, @@ -2451,6 +2583,17 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): tags = TagField( required=False ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) class PowerPortBulkCreateForm( @@ -2538,6 +2681,10 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False, @@ -2568,6 +2715,18 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form): ) self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class PowerOutletBulkCreateForm( form_from_model(PowerOutlet, ['type', 'feed_leg', 'description', 'tags']), @@ -2721,7 +2880,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 = { @@ -2763,6 +2922,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2(), @@ -2843,6 +3006,19 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk) self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) + def clean(self): + + # Validate that the number of ports 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 {} interfaces, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + + class InterfaceBulkCreateForm( form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 9d94e0639..d35504368 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -58,6 +58,10 @@ Name {{ interface.name }} + + Label + {{ interface.label }} + Type {{ interface.get_type_display }} diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index a80b7c592..eaffe2bca 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -6,6 +6,7 @@
Interface
{% render_field form.name %} + {% render_field form.label %} {% render_field form.type %} {% render_field form.enabled %} {% render_field form.lag %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 4b5993c5f..462e45819 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -919,21 +919,26 @@ class ComponentCreateView(GetReturnURLMixin, 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 pos, name in enumerate(names): + label = labels[pos] 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)) + data.update(form.get_iterative_data(pos)) component_form = self.model_form(data) if component_form.is_valid(): 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' + if field == 'label': + field = 'label_pattern' for e in errors: form.add_error(field, '{}: {}'.format(name, ', '.join(e))) @@ -1003,10 +1008,14 @@ class BulkComponentCreateView(GetReturnURLMixin, View): for obj in data['pk']: names = data['name_pattern'] - for name in names: + labels = data['label_pattern'] + for pos, name in enumerate(names): + label = labels[pos] 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)