From 99d5013de30330a2c943ddb0e3030db0e49f6258 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Dec 2021 13:26:17 -0500 Subject: [PATCH] Initial work on #7846 --- netbox/dcim/api/serializers.py | 19 +++++++++++++-- netbox/dcim/constants.py | 21 ++++++++++++++--- netbox/dcim/filtersets.py | 2 ++ netbox/dcim/forms/models.py | 20 +++++++++++++--- netbox/dcim/forms/object_create.py | 16 ++++++++++--- .../0147_inventoryitem_component.py | 23 +++++++++++++++++++ netbox/dcim/models/device_components.py | 22 ++++++++++++++++++ netbox/dcim/tables/devices.py | 15 ++++++++---- netbox/dcim/tests/test_api.py | 19 ++++++++++++--- netbox/dcim/tests/test_filtersets.py | 16 ++++++++++--- netbox/templates/dcim/inventoryitem.html | 10 ++++++++ 11 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 netbox/dcim/migrations/0147_inventoryitem_component.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5e07ea3fd0e..30f451e8458 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -810,17 +810,32 @@ class InventoryItemSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') device = NestedDeviceSerializer() parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) - manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) role = NestedInventoryItemRoleSerializer(required=False, allow_null=True) + manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) _depth = serializers.IntegerField(source='level', read_only=True) class Meta: model = InventoryItem fields = [ 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', - 'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', + 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', + 'custom_fields', 'created', 'last_updated', '_depth', ] + @swagger_serializer_method(serializer_or_field=serializers.DictField) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix='Nested') + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data + # # Device component roles diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 2136f06aab5..00126ebf82c 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -50,16 +50,31 @@ # -# PowerFeeds +# Power feeds # POWERFEED_VOLTAGE_DEFAULT = 120 - POWERFEED_AMPERAGE_DEFAULT = 20 - POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage +# +# Device components +# + +MODULAR_COMPONENT_MODELS = Q( + app_label='dcim', + model__in=( + 'consoleport', + 'consoleserverport', + 'frontport', + 'interface', + 'poweroutlet', + 'powerport', + 'rearport', + )) + + # # Cabling and connections # diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 01c0a278d21..14a2ae3eea7 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1294,6 +1294,8 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): to_field_name='slug', label='Role (slug)', ) + component_type = ContentTypeFilter() + component_id = MultiValueNumberFilter() serial = django_filters.CharFilter( lookup_expr='iexact' ) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 2be571f71eb..b193f76d5d5 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -12,8 +12,8 @@ from ipam.models import IPAddress, VLAN, VLANGroup, ASN from tenancy.forms import TenancyForm from utilities.forms import ( - APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea, + APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect, ) from virtualization.models import Cluster, ClusterGroup @@ -1376,6 +1376,15 @@ class InventoryItemForm(CustomFieldModelForm): queryset=Manufacturer.objects.all(), required=False ) + component_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=MODULAR_COMPONENT_MODELS, + required=False, + widget=StaticSelect + ) + component_id = forms.IntegerField( + required=False + ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1385,8 +1394,13 @@ class Meta: model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'description', 'tags', + 'description', 'component_type', 'component_id', 'tags', ] + fieldsets = ( + ('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), + ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')), + ('Component', ('component_type', 'component_id')), + ) # diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 9e208300b2d..03c48886556 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.contenttypes.models import ContentType from dcim.choices import * from dcim.constants import * @@ -7,8 +8,8 @@ from extras.models import Tag from ipam.models import VLAN from utilities.forms import ( - add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - ExpandableNameField, StaticSelect, + add_blank_choice, BootstrapMixin, ColorField, ContentTypeChoiceField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, ExpandableNameField, StaticSelect, ) from wireless.choices import * from .common import InterfaceCommonForm @@ -680,7 +681,16 @@ class InventoryItemCreateForm(ComponentCreateForm): max_length=50, required=False, ) + component_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=MODULAR_COMPONENT_MODELS, + required=False, + widget=StaticSelect + ) + component_id = forms.IntegerField( + required=False + ) field_order = ( 'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'description', 'tags', + 'description', 'component_type', 'component_id', 'tags', ) diff --git a/netbox/dcim/migrations/0147_inventoryitem_component.py b/netbox/dcim/migrations/0147_inventoryitem_component.py new file mode 100644 index 00000000000..36085c35d2b --- /dev/null +++ b/netbox/dcim/migrations/0147_inventoryitem_component.py @@ -0,0 +1,23 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('dcim', '0146_inventoryitemrole'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryitem', + name='component_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='inventoryitem', + name='component_type', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index cb38d86830a..cdfaa7c89ec 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -97,6 +97,12 @@ class ModularComponentModel(ComponentModel): blank=True, null=True ) + inventory_items = GenericRelation( + to='dcim.InventoryItem', + content_type_field='component_type', + object_id_field='component_id', + related_name='%(class)ss', + ) class Meta: abstract = True @@ -994,6 +1000,22 @@ class InventoryItem(MPTTModel, ComponentModel): null=True, db_index=True ) + component_type = models.ForeignKey( + to=ContentType, + limit_choices_to=MODULAR_COMPONENT_MODELS, + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + component_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + component = GenericForeignKey( + ct_field='component_type', + fk_field='component_id' + ) role = models.ForeignKey( to='dcim.InventoryItemRole', on_delete=models.PROTECT, diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 9472be541cb..4eda4a9377a 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -780,6 +780,9 @@ class InventoryItemTable(DeviceComponentTable): manufacturer = tables.Column( linkify=True ) + component = tables.Column( + linkify=True + ) discovered = BooleanColumn() tags = TagColumn( url_name='dcim:inventoryitem_list' @@ -790,9 +793,11 @@ class Meta(BaseTable.Meta): model = InventoryItem fields = ( 'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'description', 'discovered', 'tags', + 'component', 'description', 'discovered', 'tags', + ) + default_columns = ( + 'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', ) - default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag') class DeviceInventoryItemTable(InventoryItemTable): @@ -810,11 +815,11 @@ class DeviceInventoryItemTable(InventoryItemTable): class Meta(BaseTable.Meta): model = InventoryItem fields = ( - 'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', - 'discovered', 'tags', 'actions', + 'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', + 'description', 'discovered', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions', + 'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'actions', ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index e6ea1049975..b3c41e277fd 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1632,9 +1632,16 @@ def setUpTestData(cls): ) InventoryItemRole.objects.bulk_create(roles) - InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer) - InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer) - InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer) + interfaces = ( + Interface(device=device, name='Interface 1'), + Interface(device=device, name='Interface 2'), + Interface(device=device, name='Interface 3'), + ) + Interface.objects.bulk_create(interfaces) + + InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer, component=interfaces[0]) + InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer, component=interfaces[1]) + InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer, component=interfaces[2]) cls.create_data = [ { @@ -1642,18 +1649,24 @@ def setUpTestData(cls): 'name': 'Inventory Item 4', 'role': roles[1].pk, 'manufacturer': manufacturer.pk, + 'component_type': 'dcim.interface', + 'component_id': interfaces[0].pk, }, { 'device': device.pk, 'name': 'Inventory Item 5', 'role': roles[1].pk, 'manufacturer': manufacturer.pk, + 'component_type': 'dcim.interface', + 'component_id': interfaces[1].pk, }, { 'device': device.pk, 'name': 'Inventory Item 6', 'role': roles[1].pk, 'manufacturer': manufacturer.pk, + 'component_type': 'dcim.interface', + 'component_id': interfaces[2].pk, }, ] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index a808aeda2a0..f53705336b2 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -3004,10 +3004,16 @@ def setUpTestData(cls): ) InventoryItemRole.objects.bulk_create(roles) + components = ( + Interface.objects.create(device=devices[0], name='Interface 1'), + ConsolePort.objects.create(device=devices[1], name='Console Port 1'), + ConsoleServerPort.objects.create(device=devices[2], name='Console Server Port 1'), + ) + inventory_items = ( - InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'), - InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'), - InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'), + InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First', component=components[0]), + InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second', component=components[1]), + InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third', component=components[2]), ) for i in inventory_items: i.save() @@ -3103,6 +3109,10 @@ def test_serial(self): params = {'serial': 'abc'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_component_type(self): + params = {'component_type': 'dcim.interface'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = InventoryItemRole.objects.all() diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 7de30365602..0e30c5c8cc3 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -50,6 +50,16 @@
Inventory Item
{% endif %} + + Component + + {% if object.component %} + {{ object.component }} + {% else %} + + {% endif %} + + Manufacturer