From bb30f64252c4a22e611101886ec34552f97014d6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Oct 2019 14:25:55 -0400 Subject: [PATCH] Closes #1865: Add console port and console server port types --- docs/release-notes/version-2.7.md | 7 +- netbox/dcim/api/serializers.py | 16 ++-- netbox/dcim/api/views.py | 5 +- netbox/dcim/constants.py | 96 +++++++++++++++++++ netbox/dcim/filters.py | 16 +++- netbox/dcim/forms.py | 49 ++++++++-- .../migrations/0076_console_port_types.py | 33 +++++++ netbox/dcim/models.py | 32 ++++++- netbox/dcim/tables.py | 4 +- netbox/dcim/tests/test_views.py | 8 ++ netbox/templates/dcim/device.html | 1 + netbox/templates/dcim/inc/consoleport.html | 5 + .../templates/dcim/inc/consoleserverport.html | 5 + 13 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 netbox/dcim/migrations/0076_console_port_types.py diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index adb0aa636..e51cdb644 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -85,11 +85,16 @@ Full connection details are required in both sections, even if they are the same ## Enhancements +* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types * [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace supervisord with systemd * [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster * [#3538](https://github.com/digitalocean/netbox/issues/3538) - ## API Changes -* Introduced `/api/extras/scripts/` endpoint for retreiving and executing custom scripts +* Introduced `/api/extras/scripts/` endpoint for retrieving and executing custom scripts +* dcim.ConsolePort: Added field `type` +* dcim.ConsolePortTemplate: Added field `type` +* dcim.ConsoleServerPort: Added field `type` +* dcim.ConsoleServerPortTemplate: Added field `type` * virtualization.Cluster: Added field `tenant` diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 6ec2df80d..5baae4528 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -200,18 +200,20 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField(choices=CONSOLE_TYPE_CHOICES, required=False) class Meta: model = ConsolePortTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'type'] class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField(choices=CONSOLE_TYPE_CHOICES, required=False) class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'type'] class PowerPortTemplateSerializer(ValidatedModelSerializer): @@ -370,27 +372,29 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField(choices=CONSOLE_TYPE_CHOICES, required=False) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = ConsoleServerPort fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'connection_status', 'cable', 'tags', ] class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField(choices=CONSOLE_TYPE_CHOICES, required=False) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = ConsolePort fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'connection_status', 'cable', 'tags', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 12774e4be..4755ea8d9 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -42,7 +42,10 @@ from .exceptions import MissingFilterException class DCIMFieldChoicesViewSet(FieldChoicesViewSet): fields = ( (Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), - (ConsolePort, ['connection_status']), + (ConsolePort, ['type', 'connection_status']), + (ConsolePortTemplate, ['type']), + (ConsoleServerPort, ['type']), + (ConsoleServerPortTemplate, ['type']), (Device, ['face', 'status']), (DeviceType, ['subdevice_role']), (FrontPort, ['type']), diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 02662d9f8..f6829d413 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -57,6 +57,41 @@ SUBDEVICE_ROLE_CHOICES = ( (SUBDEVICE_ROLE_CHILD, 'Child'), ) +# +# Numeric console port types +# + +CONSOLE_TYPE_DE9 = 1000 +CONSOLE_TYPE_DB25 = 1100 +CONSOLE_TYPE_RJ45 = 2000 +CONSOLE_TYPE_USB_A = 3000 +CONSOLE_TYPE_USB_B = 3010 +CONSOLE_TYPE_USB_C = 3020 +CONSOLE_TYPE_USB_MINI_A = 3100 +CONSOLE_TYPE_USB_MINI_B = 3110 +CONSOLE_TYPE_USB_MICRO_A = 3200 +CONSOLE_TYPE_USB_MICRO_B = 3210 +CONSOLE_TYPE_OTHER = 32767 +CONSOLE_TYPE_CHOICES = [ + ['Serial', [ + [CONSOLE_TYPE_DE9, 'DE-9'], + [CONSOLE_TYPE_DB25, 'DB-25'], + [CONSOLE_TYPE_RJ45, 'RJ-45'], + ]], + ['USB', [ + [CONSOLE_TYPE_USB_A, 'USB Type A'], + [CONSOLE_TYPE_USB_B, 'USB Type B'], + [CONSOLE_TYPE_USB_C, 'USB Type C'], + [CONSOLE_TYPE_USB_MINI_A, 'USB Mini A'], + [CONSOLE_TYPE_USB_MINI_B, 'USB Mini B'], + [CONSOLE_TYPE_USB_MICRO_A, 'USB Micro A'], + [CONSOLE_TYPE_USB_MICRO_B, 'USB Micro B'], + ]], + ['Other', [ + [CONSOLE_TYPE_OTHER, 'Other'], + ]], +] + # # Numeric interface types # @@ -515,6 +550,67 @@ POWERFEED_LEG_CHOICES = ( ) +# +# Console port type values +# + +class ConsolePortTypes: + """ + ConsolePort/ConsoleServerPort.type slugs + """ + TYPE_DE9 = 'de-9' + TYPE_DB25 = 'db-25' + TYPE_RJ45 = 'rj-45' + TYPE_USB_A = 'usb-a' + TYPE_USB_B = 'usb-b' + TYPE_USB_C = 'usb-c' + TYPE_USB_MINI_A = 'usb-mini-a' + TYPE_USB_MINI_B = 'usb-mini-b' + TYPE_USB_MICRO_A = 'usb-micro-a' + TYPE_USB_MICRO_B = 'usb-micro-b' + TYPE_OTHER = 'other' + + TYPE_CHOICES = ( + ('Serial', ( + (TYPE_DE9, 'DE-9'), + (TYPE_DB25, 'DB-25'), + (TYPE_RJ45, 'RJ-45'), + )), + ('USB', ( + (TYPE_USB_A, 'USB Type A'), + (TYPE_USB_B, 'USB Type B'), + (TYPE_USB_C, 'USB Type C'), + (TYPE_USB_MINI_A, 'USB Mini A'), + (TYPE_USB_MINI_B, 'USB Mini B'), + (TYPE_USB_MICRO_A, 'USB Micro A'), + (TYPE_USB_MICRO_B, 'USB Micro B'), + )), + ('Other', ( + (TYPE_OTHER, 'Other'), + )), + ) + + @classmethod + def slug_to_integer(cls, slug): + """ + Provide backward-compatible mapping of the type slug to integer. + """ + return { + # Slug: integer + cls.TYPE_DE9: CONSOLE_TYPE_DE9, + cls.TYPE_DB25: CONSOLE_TYPE_DB25, + cls.TYPE_RJ45: CONSOLE_TYPE_RJ45, + cls.TYPE_USB_A: CONSOLE_TYPE_USB_A, + cls.TYPE_USB_B: CONSOLE_TYPE_USB_B, + cls.TYPE_USB_C: CONSOLE_TYPE_USB_C, + cls.TYPE_USB_MINI_A: CONSOLE_TYPE_USB_MINI_A, + cls.TYPE_USB_MINI_B: CONSOLE_TYPE_USB_MINI_B, + cls.TYPE_USB_MICRO_A: CONSOLE_TYPE_USB_MICRO_A, + cls.TYPE_USB_MICRO_B: CONSOLE_TYPE_USB_MICRO_B, + cls.TYPE_OTHER: CONSOLE_TYPE_OTHER, + }.get(slug) + + # # Interface type values # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index c1274e3d5..49e03742a 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -346,14 +346,14 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate - fields = ['id', 'name'] + fields = ['id', 'name', 'type'] class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'name'] + fields = ['id', 'name', 'type'] class PowerPortTemplateFilter(DeviceTypeComponentFilterSet): @@ -641,6 +641,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet): class ConsolePortFilter(DeviceComponentFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=CONSOLE_TYPE_CHOICES, + null_value=None + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -649,10 +653,14 @@ class ConsolePortFilter(DeviceComponentFilterSet): class Meta: model = ConsolePort - fields = ['id', 'name', 'description', 'connection_status'] + fields = ['id', 'name', 'type', 'description', 'connection_status'] class ConsoleServerPortFilter(DeviceComponentFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=CONSOLE_TYPE_CHOICES, + null_value=None + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -661,7 +669,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet): class Meta: model = ConsoleServerPort - fields = ['id', 'name', 'description', 'connection_status'] + fields = ['id', 'name', 'type', 'description', 'connection_status'] class PowerPortFilter(DeviceComponentFilterSet): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6513cfee2..7d1757e74 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -941,7 +941,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -952,6 +952,10 @@ class ConsolePortTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=CONSOLE_TYPE_CHOICES, + widget=StaticSelect2() + ) class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): @@ -959,7 +963,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -970,6 +974,10 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=CONSOLE_TYPE_CHOICES, + widget=StaticSelect2() + ) class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): @@ -1248,22 +1256,38 @@ class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): class ConsolePortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=ConsolePortTypes.TYPE_CHOICES + ) class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] + def clean_type(self): + # Convert slug value to field integer value + slug = self.cleaned_data['type'] + return ConsolePortTypes.slug_to_integer(slug) + class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=ConsolePortTypes.TYPE_CHOICES + ) class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] + def clean_type(self): + # Convert slug value to field integer value + slug = self.cleaned_data['type'] + return ConsolePortTypes.slug_to_integer(slug) + class PowerPortTemplateImportForm(ComponentTemplateImportForm): @@ -2054,7 +2078,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePort fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'type', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2065,6 +2089,10 @@ class ConsolePortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=CONSOLE_TYPE_CHOICES, + widget=StaticSelect2(), + ) description = forms.CharField( max_length=100, required=False @@ -2086,7 +2114,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPort fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'type', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2097,6 +2125,10 @@ class ConsoleServerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=CONSOLE_TYPE_CHOICES, + widget=StaticSelect2(), + ) description = forms.CharField( max_length=100, required=False @@ -2111,6 +2143,11 @@ class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditF queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput() ) + type = forms.ChoiceField( + choices=add_blank_choice(CONSOLE_TYPE_CHOICES), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False diff --git a/netbox/dcim/migrations/0076_console_port_types.py b/netbox/dcim/migrations/0076_console_port_types.py new file mode 100644 index 000000000..bed31445d --- /dev/null +++ b/netbox/dcim/migrations/0076_console_port_types.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.6 on 2019-10-30 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0075_cable_devices'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='type', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='consoleporttemplate', + name='type', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='consoleserverport', + name='type', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='consoleserverporttemplate', + name='type', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b7c5ad9e4..ac958e363 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1014,6 +1014,11 @@ class ConsolePortTemplate(ComponentTemplateModel): name = models.CharField( max_length=50 ) + type = models.PositiveSmallIntegerField( + choices=CONSOLE_TYPE_CHOICES, + blank=True, + null=True + ) objects = NaturalOrderingManager() @@ -1027,7 +1032,8 @@ class ConsolePortTemplate(ComponentTemplateModel): def instantiate(self, device): return ConsolePort( device=device, - name=self.name + name=self.name, + type=self.type ) @@ -1043,6 +1049,11 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): name = models.CharField( max_length=50 ) + type = models.PositiveSmallIntegerField( + choices=CONSOLE_TYPE_CHOICES, + blank=True, + null=True + ) objects = NaturalOrderingManager() @@ -1056,7 +1067,8 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): def instantiate(self, device): return ConsoleServerPort( device=device, - name=self.name + name=self.name, + type=self.type ) @@ -1846,6 +1858,11 @@ class ConsolePort(CableTermination, ComponentModel): name = models.CharField( max_length=50 ) + type = models.PositiveSmallIntegerField( + choices=CONSOLE_TYPE_CHOICES, + blank=True, + null=True + ) connected_endpoint = models.OneToOneField( to='dcim.ConsoleServerPort', on_delete=models.SET_NULL, @@ -1861,7 +1878,7 @@ class ConsolePort(CableTermination, ComponentModel): objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name', 'description'] + csv_headers = ['device', 'name', 'type', 'description'] class Meta: ordering = ['device', 'name'] @@ -1877,6 +1894,7 @@ class ConsolePort(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.type, self.description, ) @@ -1897,6 +1915,11 @@ class ConsoleServerPort(CableTermination, ComponentModel): name = models.CharField( max_length=50 ) + type = models.PositiveSmallIntegerField( + choices=CONSOLE_TYPE_CHOICES, + blank=True, + null=True + ) connection_status = models.NullBooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True @@ -1905,7 +1928,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name', 'description'] + csv_headers = ['device', 'name', 'type', 'description'] class Meta: unique_together = ['device', 'name'] @@ -1920,6 +1943,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.type, self.description, ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 70a9aa5c8..0febcb543 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -422,7 +422,7 @@ class ConsolePortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = ConsolePortTemplate - fields = ('pk', 'name', 'actions') + fields = ('pk', 'name', 'type', 'actions') empty_text = "None" @@ -647,7 +647,7 @@ class ConsolePortTable(BaseTable): class Meta(BaseTable.Meta): model = ConsolePort - fields = ('name',) + fields = ('name', 'type') class ConsoleServerPortTable(BaseTable): diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index d4572cc39..71c1be5a2 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -231,12 +231,18 @@ slug: test-1000 u_height: 2 console-ports: - name: Console Port 1 + type: de-9 - name: Console Port 2 + type: de-9 - name: Console Port 3 + type: de-9 console-server-ports: - name: Console Server Port 1 + type: rj-45 - name: Console Server Port 2 + type: rj-45 - name: Console Server Port 3 + type: rj-45 power-ports: - name: Power Port 1 - name: Power Port 2 @@ -313,10 +319,12 @@ device-bays: self.assertEqual(dt.consoleport_templates.count(), 3) cp1 = ConsolePortTemplate.objects.first() self.assertEqual(cp1.name, 'Console Port 1') + self.assertEqual(cp1.type, CONSOLE_TYPE_DE9) self.assertEqual(dt.consoleserverport_templates.count(), 3) csp1 = ConsoleServerPortTemplate.objects.first() self.assertEqual(csp1.name, 'Console Server Port 1') + self.assertEqual(csp1.type, CONSOLE_TYPE_RJ45) self.assertEqual(dt.powerport_templates.count(), 3) pp1 = PowerPortTemplate.objects.first() diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 57e2b03b8..387baaa97 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -628,6 +628,7 @@ {% endif %} Name + Type Description Cable Connection diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 03c28c22a..c78e5ed4a 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -6,6 +6,11 @@ + {# Type #} + + {% if cp.type %}{{ cp.get_type_display }}{% else %}—{% endif %} + + {# Description #} {{ cp.description }} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index 8e5666852..f5b19ed75 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -14,6 +14,11 @@ {{ csp }} + {# Type #} + + {% if csp.type %}{{ csp.get_type_display }}{% else %}—{% endif %} + + {# Description #} {{ csp.description|placeholder }}