Browse Source

Renamed Module to InventoryItem (prep for #824)

Jeremy Stretch 8 years ago
parent
commit
22768ff6c6

+ 5 - 5
docs/data-model/dcim.md

@@ -93,9 +93,12 @@ A device's platform is used to denote the type of software running on it. This c
 
 The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
 
-### Modules
+### Inventory Items
 
-A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each module can optionally be assigned to a manufacturer.
+Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each item can optionally be assigned a manufacturer.
+
+!!! note
+    Prior to version 2.0, inventory items were called modules.
 
 ### Components
 
@@ -113,6 +116,3 @@ Console ports connect only to console server ports, and power ports connect only
 Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
 
 Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
-
-!!! note
-    Child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply, which do not provide their own management plane.

+ 4 - 4
netbox/dcim/admin.py

@@ -5,7 +5,7 @@ from mptt.admin import MPTTModelAdmin
 
 from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
-    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, InventoryItem, Platform,
     PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
     Site,
 )
@@ -183,8 +183,8 @@ class DeviceBayAdmin(admin.TabularInline):
     readonly_fields = ['installed_device']
 
 
-class ModuleAdmin(admin.TabularInline):
-    model = Module
+class InventoryItemAdmin(admin.TabularInline):
+    model = InventoryItem
     readonly_fields = ['parent', 'discovered']
 
 
@@ -197,7 +197,7 @@ class DeviceAdmin(admin.ModelAdmin):
         PowerOutletAdmin,
         InterfaceAdmin,
         DeviceBayAdmin,
-        ModuleAdmin,
+        InventoryItemAdmin,
     ]
     list_display = ['display_name', 'device_type_full_name', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
                     'serial']

+ 6 - 6
netbox/dcim/api/serializers.py

@@ -5,7 +5,7 @@ from ipam.models import IPAddress
 from dcim.models import (
     CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
     DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
-    InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
+    InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
     PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
     RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
 )
@@ -642,22 +642,22 @@ class WritableDeviceBaySerializer(serializers.ModelSerializer):
 
 
 #
-# Modules
+# Inventory items
 #
 
-class ModuleSerializer(serializers.ModelSerializer):
+class InventoryItemSerializer(serializers.ModelSerializer):
     device = NestedDeviceSerializer()
     manufacturer = NestedManufacturerSerializer()
 
     class Meta:
-        model = Module
+        model = InventoryItem
         fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
 
 
-class WritableModuleSerializer(serializers.ModelSerializer):
+class WritableInventoryItemSerializer(serializers.ModelSerializer):
 
     class Meta:
-        model = Module
+        model = InventoryItem
         fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
 
 

+ 1 - 1
netbox/dcim/api/urls.py

@@ -48,7 +48,7 @@ router.register(r'power-ports', views.PowerPortViewSet)
 router.register(r'power-outlets', views.PowerOutletViewSet)
 router.register(r'interfaces', views.InterfaceViewSet)
 router.register(r'device-bays', views.DeviceBayViewSet)
-router.register(r'modules', views.ModuleViewSet)
+router.register(r'inventory-items', views.InventoryItemViewSet)
 
 # Interface connections
 router.register(r'interface-connections', views.InterfaceConnectionViewSet)

+ 6 - 6
netbox/dcim/api/views.py

@@ -8,7 +8,7 @@ from django.shortcuts import get_object_or_404
 
 from dcim.models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
-    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
     Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
     RackRole, Region, Site,
 )
@@ -294,11 +294,11 @@ class DeviceBayViewSet(WritableSerializerMixin, ModelViewSet):
     filter_class = filters.DeviceBayFilter
 
 
-class ModuleViewSet(WritableSerializerMixin, ModelViewSet):
-    queryset = Module.objects.select_related('device', 'manufacturer')
-    serializer_class = serializers.ModuleSerializer
-    write_serializer_class = serializers.WritableModuleSerializer
-    filter_class = filters.ModuleFilter
+class InventoryItemViewSet(WritableSerializerMixin, ModelViewSet):
+    queryset = InventoryItem.objects.select_related('device', 'manufacturer')
+    serializer_class = serializers.InventoryItemSerializer
+    write_serializer_class = serializers.WritableInventoryItemSerializer
+    filter_class = filters.InventoryItemFilter
 
 
 #

+ 4 - 4
netbox/dcim/filters.py

@@ -9,7 +9,7 @@ from utilities.filters import NullableModelMultipleChoiceFilter
 from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
+    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
     RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
 )
 
@@ -359,7 +359,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         return queryset.filter(
             Q(name__icontains=value) |
             Q(serial__icontains=value.strip()) |
-            Q(modules__serial__icontains=value.strip()) |
+            Q(inventory_items__serial__icontains=value.strip()) |
             Q(asset_tag=value.strip()) |
             Q(comments__icontains=value)
         ).distinct()
@@ -444,10 +444,10 @@ class DeviceBayFilter(DeviceComponentFilterSet):
         fields = ['name']
 
 
-class ModuleFilter(DeviceComponentFilterSet):
+class InventoryItemFilter(DeviceComponentFilterSet):
 
     class Meta:
-        model = Module
+        model = InventoryItem
         fields = ['name']
 
 

+ 4 - 4
netbox/dcim/forms.py

@@ -21,7 +21,7 @@ from .models import (
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
     Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
+    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
     RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
     VIRTUAL_IFACE_TYPES
 )
@@ -1684,11 +1684,11 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
 
 
 #
-# Modules
+# Inventory items
 #
 
-class ModuleForm(BootstrapMixin, forms.ModelForm):
+class InventoryItemForm(BootstrapMixin, forms.ModelForm):
 
     class Meta:
-        model = Module
+        model = InventoryItem
         fields = ['name', 'manufacturer', 'part_id', 'serial']

+ 35 - 0
netbox/dcim/migrations/0034_rename_module_to_inventoryitem.py

@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.6 on 2017-03-21 14:55
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0033_rackreservation_rack_editable'),
+    ]
+
+    operations = [
+        migrations.RenameModel(
+            old_name='Module',
+            new_name='InventoryItem',
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='device',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='parent',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='manufacturer',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'),
+        ),
+    ]

+ 7 - 7
netbox/dcim/models.py

@@ -1397,19 +1397,19 @@ class DeviceBay(models.Model):
 
 
 #
-# Modules
+# Inventory items
 #
 
 @python_2_unicode_compatible
-class Module(models.Model):
+class InventoryItem(models.Model):
     """
-    A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
-    for inventory purposes.
+    An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
+    InventoryItems are used only for inventory purposes.
     """
-    device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
-    parent = models.ForeignKey('self', related_name='submodules', blank=True, null=True, on_delete=models.CASCADE)
+    device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
+    parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
     name = models.CharField(max_length=50, verbose_name='Name')
-    manufacturer = models.ForeignKey('Manufacturer', related_name='modules', blank=True, null=True,
+    manufacturer = models.ForeignKey('Manufacturer', related_name='inventory_items', blank=True, null=True,
                                      on_delete=models.PROTECT)
     part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
     serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)

+ 33 - 33
netbox/dcim/tests/test_api.py

@@ -7,7 +7,7 @@ from django.urls import reverse
 from dcim.models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, Module, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
+    Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
     RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
 )
 from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
@@ -1847,7 +1847,7 @@ class DeviceBayTest(HttpStatusMixin, APITestCase):
         self.assertEqual(DeviceBay.objects.count(), 2)
 
 
-class ModuleTest(HttpStatusMixin, APITestCase):
+class InventoryItemTest(HttpStatusMixin, APITestCase):
 
     def setUp(self):
 
@@ -1866,71 +1866,71 @@ class ModuleTest(HttpStatusMixin, APITestCase):
         self.device = Device.objects.create(
             device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
         )
-        self.module1 = Module.objects.create(device=self.device, name='Test Module 1')
-        self.module2 = Module.objects.create(device=self.device, name='Test Module 2')
-        self.module3 = Module.objects.create(device=self.device, name='Test Module 3')
+        self.inventoryitem1 = InventoryItem.objects.create(device=self.device, name='Test Inventory Item 1')
+        self.inventoryitem2 = InventoryItem.objects.create(device=self.device, name='Test Inventory Item 2')
+        self.inventoryitem3 = InventoryItem.objects.create(device=self.device, name='Test Inventory Item 3')
 
-    def test_get_module(self):
+    def test_get_inventoryitem(self):
 
-        url = reverse('dcim-api:module-detail', kwargs={'pk': self.module1.pk})
+        url = reverse('dcim-api:inventoryitem-detail', kwargs={'pk': self.inventoryitem1.pk})
         response = self.client.get(url, **self.header)
 
-        self.assertEqual(response.data['name'], self.module1.name)
+        self.assertEqual(response.data['name'], self.inventoryitem1.name)
 
-    def test_list_modules(self):
+    def test_list_inventoryitems(self):
 
-        url = reverse('dcim-api:module-list')
+        url = reverse('dcim-api:inventoryitem-list')
         response = self.client.get(url, **self.header)
 
         self.assertEqual(response.data['count'], 3)
 
-    def test_create_module(self):
+    def test_create_inventoryitem(self):
 
         data = {
             'device': self.device.pk,
-            'parent': self.module1.pk,
-            'name': 'Test Module 4',
+            'parent': self.inventoryitem1.pk,
+            'name': 'Test Inventory Item 4',
             'manufacturer': self.manufacturer.pk,
         }
 
-        url = reverse('dcim-api:module-list')
+        url = reverse('dcim-api:inventoryitem-list')
         response = self.client.post(url, data, **self.header)
 
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
-        self.assertEqual(Module.objects.count(), 4)
-        module4 = Module.objects.get(pk=response.data['id'])
-        self.assertEqual(module4.device_id, data['device'])
-        self.assertEqual(module4.parent_id, data['parent'])
-        self.assertEqual(module4.name, data['name'])
-        self.assertEqual(module4.manufacturer_id, data['manufacturer'])
+        self.assertEqual(InventoryItem.objects.count(), 4)
+        inventoryitem4 = InventoryItem.objects.get(pk=response.data['id'])
+        self.assertEqual(inventoryitem4.device_id, data['device'])
+        self.assertEqual(inventoryitem4.parent_id, data['parent'])
+        self.assertEqual(inventoryitem4.name, data['name'])
+        self.assertEqual(inventoryitem4.manufacturer_id, data['manufacturer'])
 
-    def test_update_module(self):
+    def test_update_inventoryitem(self):
 
         data = {
             'device': self.device.pk,
-            'parent': self.module1.pk,
-            'name': 'Test Module X',
+            'parent': self.inventoryitem1.pk,
+            'name': 'Test Inventory Item X',
             'manufacturer': self.manufacturer.pk,
         }
 
-        url = reverse('dcim-api:module-detail', kwargs={'pk': self.module1.pk})
+        url = reverse('dcim-api:inventoryitem-detail', kwargs={'pk': self.inventoryitem1.pk})
         response = self.client.put(url, data, **self.header)
 
         self.assertHttpStatus(response, status.HTTP_200_OK)
-        self.assertEqual(Module.objects.count(), 3)
-        module1 = Module.objects.get(pk=response.data['id'])
-        self.assertEqual(module1.device_id, data['device'])
-        self.assertEqual(module1.parent_id, data['parent'])
-        self.assertEqual(module1.name, data['name'])
-        self.assertEqual(module1.manufacturer_id, data['manufacturer'])
+        self.assertEqual(InventoryItem.objects.count(), 3)
+        inventoryitem1 = InventoryItem.objects.get(pk=response.data['id'])
+        self.assertEqual(inventoryitem1.device_id, data['device'])
+        self.assertEqual(inventoryitem1.parent_id, data['parent'])
+        self.assertEqual(inventoryitem1.name, data['name'])
+        self.assertEqual(inventoryitem1.manufacturer_id, data['manufacturer'])
 
-    def test_delete_module(self):
+    def test_delete_inventoryitem(self):
 
-        url = reverse('dcim-api:module-detail', kwargs={'pk': self.module1.pk})
+        url = reverse('dcim-api:inventoryitem-detail', kwargs={'pk': self.inventoryitem1.pk})
         response = self.client.delete(url, **self.header)
 
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
-        self.assertEqual(Module.objects.count(), 2)
+        self.assertEqual(InventoryItem.objects.count(), 2)
 
 
 class InterfaceConnectionTest(HttpStatusMixin, APITestCase):

+ 5 - 5
netbox/dcim/urls.py

@@ -173,6 +173,11 @@ urlpatterns = [
     url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
     url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
 
+    # Inventory items
+    url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
+    url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
+    url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
+
     # Console/power/interface connections
     url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
     url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
@@ -181,9 +186,4 @@ urlpatterns = [
     url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
     url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
 
-    # Modules
-    url(r'^devices/(?P<device>\d+)/modules/add/$', views.ModuleEditView.as_view(), name='module_add'),
-    url(r'^modules/(?P<pk>\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
-    url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'),
-
 ]

+ 12 - 12
netbox/dcim/views.py

@@ -25,7 +25,7 @@ from . import filters, forms, tables
 from .models import (
     CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
     DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
+    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
     RackReservation, RackRole, Region, Site,
 )
 
@@ -799,12 +799,12 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 def device_inventory(request, pk):
 
     device = get_object_or_404(Device, pk=pk)
-    modules = Module.objects.filter(device=device, parent=None).select_related('manufacturer')\
-        .prefetch_related('submodules')
+    inventory_items = InventoryItem.objects.filter(device=device, parent=None).select_related('manufacturer')\
+        .prefetch_related('child_items')
 
     return render(request, 'dcim/device_inventory.html', {
         'device': device,
-        'modules': modules,
+        'inventory_items': inventory_items,
     })
 
 
@@ -1594,13 +1594,13 @@ def ipaddress_assign(request, pk):
 
 
 #
-# Modules
+# Inventory items
 #
 
-class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
-    permission_required = 'dcim.change_module'
-    model = Module
-    form_class = forms.ModuleForm
+class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
+    permission_required = 'dcim.change_inventoryitem'
+    model = InventoryItem
+    form_class = forms.InventoryItemForm
 
     def alter_obj(self, obj, request, url_args, url_kwargs):
         if 'device' in url_kwargs:
@@ -1608,6 +1608,6 @@ class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
         return obj
 
 
-class ModuleDeleteView(PermissionRequiredMixin, ComponentDeleteView):
-    permission_required = 'dcim.delete_module'
-    model = Module
+class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
+    permission_required = 'dcim.delete_inventoryitem'
+    model = InventoryItem

+ 12 - 12
netbox/extras/management/commands/run_inventory.py

@@ -6,7 +6,7 @@ from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
 from django.db import transaction
 
-from dcim.models import Device, Module, Site
+from dcim.models import Device, InventoryItem, Site
 
 
 class Command(BaseCommand):
@@ -25,12 +25,12 @@ class Command(BaseCommand):
 
     def handle(self, *args, **options):
 
-        def create_modules(modules, parent=None):
-            for module in modules:
-                m = Module(device=device, parent=parent, name=module['name'], part_id=module['part_id'],
-                           serial=module['serial'], discovered=True)
-                m.save()
-                create_modules(module.get('modules', []), parent=m)
+        def create_inventory_items(inventory_items, parent=None):
+            for item in inventory_items:
+                i = InventoryItem(device=device, parent=parent, name=item['name'], part_id=item['part_id'],
+                                  serial=item['serial'], discovered=True)
+                i.save()
+                create_inventory_items(item.get('items', []), parent=i)
 
         # Credentials
         if options['username']:
@@ -107,9 +107,9 @@ class Command(BaseCommand):
                 self.stdout.write("")
                 self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial']))
                 self.stdout.write("\tDescription: {}".format(inventory['chassis']['description']))
-                for module in inventory['modules']:
-                    self.stdout.write("\tModule: {} / {} ({})".format(module['name'], module['part_id'],
-                                                                      module['serial']))
+                for item in inventory['items']:
+                    self.stdout.write("\tItem: {} / {} ({})".format(item['name'], item['part_id'],
+                                                                    item['serial']))
             else:
                 self.stdout.write("{} ({})".format(inventory['chassis']['description'], inventory['chassis']['serial']))
 
@@ -119,7 +119,7 @@ class Command(BaseCommand):
                     if device.serial != inventory['chassis']['serial']:
                         device.serial = inventory['chassis']['serial']
                         device.save()
-                    Module.objects.filter(device=device, discovered=True).delete()
-                    create_modules(inventory.get('modules', []))
+                    InventoryItem.objects.filter(device=device, discovered=True).delete()
+                    create_inventory_items(inventory.get('items', []))
 
         self.stdout.write("Finished!")

+ 22 - 22
netbox/extras/rpc.py

@@ -33,14 +33,14 @@ class RPCClient(object):
 
     def get_inventory(self):
         """
-        Returns a dictionary representing the device chassis and installed modules.
+        Returns a dictionary representing the device chassis and installed inventory items.
 
         {
             'chassis': {
                 'serial': <str>,
                 'description': <str>,
             }
-            'modules': [
+            'items': [
                 {
                     'name': <str>,
                     'part_id': <str>,
@@ -144,23 +144,23 @@ class JunosNC(RPCClient):
 
     def get_inventory(self):
 
-        def glean_modules(node, depth=0):
-            modules = []
-            modules_list = node.get('chassis{}-module'.format('-sub' * depth), [])
+        def glean_items(node, depth=0):
+            items = []
+            items_list = node.get('chassis{}-module'.format('-sub' * depth), [])
             # Junos like to return single children directly instead of as a single-item list
-            if hasattr(modules_list, 'items'):
-                modules_list = [modules_list]
-            for module in modules_list:
+            if hasattr(items_list, 'items'):
+                items_list = [items_list]
+            for item in items_list:
                 m = {
-                    'name': module['name'],
-                    'part_id': module.get('model-number') or module.get('part-number', ''),
-                    'serial': module.get('serial-number', ''),
+                    'name': item['name'],
+                    'part_id': item.get('model-number') or item.get('part-number', ''),
+                    'serial': item.get('serial-number', ''),
                 }
-                submodules = glean_modules(module, depth + 1)
-                if submodules:
-                    m['modules'] = submodules
-                modules.append(m)
-            return modules
+                child_items = glean_items(item, depth + 1)
+                if child_items:
+                    m['items'] = child_items
+                items.append(m)
+            return items
 
         rpc_reply = self.manager.dispatch('get-chassis-inventory')
         inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
@@ -173,8 +173,8 @@ class JunosNC(RPCClient):
             'description': inventory_raw['description'],
         }
 
-        # Gather modules
-        result['modules'] = glean_modules(inventory_raw)
+        # Gather inventory items
+        result['items'] = glean_items(inventory_raw)
 
         return result
 
@@ -199,7 +199,7 @@ class IOSSSH(SSHClient):
                 'description': parse(sh_ver, 'cisco ([^\s]+)')
             }
 
-        def modules(chassis_serial=None):
+        def items(chassis_serial=None):
             cmd = self._send('show inventory').split('\r\n\r\n')
             for i in cmd:
                 i_fmt = i.replace('\r\n', ' ')
@@ -207,7 +207,7 @@ class IOSSSH(SSHClient):
                     m_name = re.search('NAME: "([^"]+)"', i_fmt).group(1)
                     m_pid = re.search('PID: ([^\s]+)', i_fmt).group(1)
                     m_serial = re.search('SN: ([^\s]+)', i_fmt).group(1)
-                    # Omit built-in modules and those with no PID
+                    # Omit built-in items and those with no PID
                     if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
                         yield {
                             'name': m_name,
@@ -222,7 +222,7 @@ class IOSSSH(SSHClient):
 
         return {
             'chassis': sh_version,
-            'modules': list(modules(chassis_serial=sh_version.get('serial')))
+            'items': list(items(chassis_serial=sh_version.get('serial')))
         }
 
 
@@ -257,7 +257,7 @@ class OpengearSSH(SSHClient):
                 'serial': serial,
                 'description': description,
             },
-            'modules': [],
+            'items': [],
         }
 
 

+ 8 - 71
netbox/templates/dcim/device_inventory.html

@@ -46,7 +46,7 @@
             <table class="table table-hover table-condensed panel-body" id="hardware">
                 <thead>
                     <tr>
-                        <th>Module</th>
+                        <th>Name</th>
                         <th></th>
                         <th>Manufacturer</th>
                         <th>Part Number</th>
@@ -55,81 +55,18 @@
                     </tr>
                 </thead>
                 <tbody>
-                    {% for m in modules %}
-                        <tr>
-                            <td>{{ m.name }}</td>
-                            <td>{% if not m.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
-                            <td>{{ m.manufacturer|default:'' }}</td>
-                            <td>{{ m.part_id }}</td>
-                            <td>{{ m.serial }}</td>
-                            <td class="text-right">
-                                {% if perms.dcim.change_module %}
-                                    <a href="{% url 'dcim:module_edit' pk=m.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
-                                {% endif %}
-                                {% if perms.dcim.delete_module %}
-                                    <a href="{% url 'dcim:module_delete' pk=m.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
-                                {% endif %}
-                            </td>
-                        </tr>
-                        {% for m2 in m.submodules.all %}
-                            <tr>
-                                <td style="padding-left: 20px">{{ m2.name }}</td>
-                                <td>{% if not m2.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
-                                <td>{{ m2.manufacturer|default:'' }}</td>
-                                <td>{{ m2.part_id }}</td>
-                                <td>{{ m2.serial }}</td>
-                                <td class="text-right">
-                                    {% if perms.dcim.change_module %}
-                                        <a href="{% url 'dcim:module_edit' pk=m2.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
-                                    {% endif %}
-                                    {% if perms.dcim.delete_module %}
-                                        <a href="{% url 'dcim:module_delete' pk=m2.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
-                                    {% endif %}
-                                </td>
-                            </tr>
-                            {% for m3 in m2.submodules.all %}
-                                <tr>
-                                    <td style="padding-left: 40px">{{ m3.name }}</td>
-                                    <td>{% if not m3.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
-                                    <td>{{ m3.manufacturer|default:'' }}</td>
-                                    <td>{{ m3.part_id }}</td>
-                                    <td>{{ m3.serial }}</td>
-                                    <td class="text-right">
-                                        {% if perms.dcim.change_module %}
-                                            <a href="{% url 'dcim:module_edit' pk=m3.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
-                                        {% endif %}
-                                        {% if perms.dcim.delete_module %}
-                                            <a href="{% url 'dcim:module_delete' pk=m3.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
-                                        {% endif %}
-                                    </td>
-                                </tr>
-                                {% for m4 in m3.submodules.all %}
-                                    <tr>
-                                        <td style="padding-left: 60px">{{ m4.name }}</td>
-                                        <td>{% if not m4.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
-                                        <td>{{ m4.manufacturer|default:'' }}</td>
-                                        <td>{{ m4.part_id }}</td>
-                                        <td>{{ m4.serial }}</td>
-                                        <td class="text-right">
-                                            {% if perms.dcim.change_module %}
-                                                <a href="{% url 'dcim:module_edit' pk=m4.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
-                                            {% endif %}
-                                            {% if perms.dcim.delete_module %}
-                                                <a href="{% url 'dcim:module_delete' pk=m4.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
-                                            {% endif %}
-                                        </td>
-                                    </tr>
-                                {% endfor %}
-                            {% endfor %}
-                        {% endfor %}
+                    {% for item in inventory_items %}
+                        {% with template_name='dcim/inc/inventoryitem.html' indent=0 %}
+                            {% include template_name %}
+                        {% endwith %}
                     {% endfor %}
                 </tbody>
             </table>
         </div>
-        {% if perms.dcim.add_module %}
-            <a href="{% url 'dcim:module_add' device=device.pk %}" class="btn btn-success">
+        {% if perms.dcim.add_inventoryitem %}
+            <a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-success">
                 <span class="fa fa-plus" aria-hidden="true"></span>
-                Add a Module
+                Add Inventory Item
             </a>
         {% endif %}
     </div>

+ 20 - 0
netbox/templates/dcim/inc/inventoryitem.html

@@ -0,0 +1,20 @@
+<tr>
+    <td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
+    <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
+    <td>{{ item.manufacturer|default:'' }}</td>
+    <td>{{ item.part_id }}</td>
+    <td>{{ item.serial }}</td>
+    <td class="text-right">
+        {% if perms.dcim.change_inventory_item %}
+            <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
+        {% endif %}
+        {% if perms.dcim.delete_inventory_item %}
+            <a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
+        {% endif %}
+    </td>
+</tr>
+{% for item in item.child_items.all %}
+    {% with template_name='dcim/inc/inventoryitem.html' indent=indent|add:20 %}
+        {% include template_name %}
+    {% endwith %}
+{% endfor %}

+ 8 - 0
netbox/templates/dcim/inventoryitem_delete.html

@@ -0,0 +1,8 @@
+{% extends 'utilities/confirmation_form.html' %}
+{% load form_helpers %}
+
+{% block title %}Delete inventory item {{ inventoryitem }}?{% endblock %}
+
+{% block message %}
+    <p>Are you sure you want to delete this inventory item from <strong>{{ inventoryitem.device }}</strong>?</p>
+{% endblock %}

+ 0 - 8
netbox/templates/dcim/module_delete.html

@@ -1,8 +0,0 @@
-{% extends 'utilities/confirmation_form.html' %}
-{% load form_helpers %}
-
-{% block title %}Delete module {{ module }}?{% endblock %}
-
-{% block message %}
-    <p>Are you sure you want to delete this module from <strong>{{ module.device }}</strong>?</p>
-{% endblock %}