jeremystretch 4 лет назад
Родитель
Сommit
99d5013de3

+ 17 - 2
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

+ 18 - 3
netbox/dcim/constants.py

@@ -50,16 +50,31 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 
 
 #
-# 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
 #

+ 2 - 0
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'
     )

+ 17 - 3
netbox/dcim/forms/models.py

@@ -12,8 +12,8 @@ from extras.models import Tag
 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 InventoryItemForm(CustomFieldModelForm):
         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')),
+        )
 
 
 #

+ 13 - 3
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.forms import CustomFieldModelForm, CustomFieldsMixin
 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',
     )

+ 23 - 0
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'),
+        ),
+    ]

+ 22 - 0
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,

+ 10 - 5
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 InventoryItemTable(DeviceComponentTable):
         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',
         )
 
 

+ 16 - 3
netbox/dcim/tests/test_api.py

@@ -1632,9 +1632,16 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
         )
         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 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
                 '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,
             },
         ]
 

+ 13 - 3
netbox/dcim/tests/test_filtersets.py

@@ -3004,10 +3004,16 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         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 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
         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()

+ 10 - 0
netbox/templates/dcim/inventoryitem.html

@@ -50,6 +50,16 @@
                                 {% endif %}
                             </td>
                         </tr>
+                        <tr>
+                            <th scope="row">Component</th>
+                            <td>
+                                {% if object.component %}
+                                    <a href="{{ object.component.get_absolute_url }}">{{ object.component }}</a>
+                                {% else %}
+                                    <span class="text-muted">&mdash;</span>
+                                {% endif %}
+                            </td>
+                        </tr>
                         <tr>
                             <th scope="row">Manufacturer</th>
                             <td>