diff --git a/docs/data-model/dcim.md b/docs/data-model/dcim.md index 618f2d8d9..74dffb320 100644 --- a/docs/data-model/dcim.md +++ b/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. diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 16f07dfcf..a1b64f235 100644 --- a/netbox/dcim/admin.py +++ b/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'] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d4e403598..e9a1bd61e 100644 --- a/netbox/dcim/api/serializers.py +++ b/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'] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 8556e4141..f648c869d 100644 --- a/netbox/dcim/api/urls.py +++ b/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) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 394fae5fa..77344f4fa 100644 --- a/netbox/dcim/api/views.py +++ b/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 # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 117cd4cf4..cdf5abc33 100644 --- a/netbox/dcim/filters.py +++ b/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'] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 5fa5865fe..a2f58f1e3 100644 --- a/netbox/dcim/forms.py +++ b/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'] diff --git a/netbox/dcim/migrations/0034_rename_module_to_inventoryitem.py b/netbox/dcim/migrations/0034_rename_module_to_inventoryitem.py new file mode 100644 index 000000000..ff430c067 --- /dev/null +++ b/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'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 351d0a2b2..f9703898f 100644 --- a/netbox/dcim/models.py +++ b/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) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index b06a4f958..ef8c6f299 100644 --- a/netbox/dcim/tests/test_api.py +++ b/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): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 7fde6e9b3..b4731df33 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -173,6 +173,11 @@ urlpatterns = [ url(r'^device-bays/(?P\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'), url(r'^device-bays/(?P\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'), + # Inventory items + url(r'^devices/(?P\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), + url(r'^inventory-items/(?P\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), + url(r'^inventory-items/(?P\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\d+)/modules/add/$', views.ModuleEditView.as_view(), name='module_add'), - url(r'^modules/(?P\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'), - url(r'^modules/(?P\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'), - ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 246fe06f0..20e51d34e 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/extras/management/commands/run_inventory.py b/netbox/extras/management/commands/run_inventory.py index ebfee92e0..a7d643173 100644 --- a/netbox/extras/management/commands/run_inventory.py +++ b/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!") diff --git a/netbox/extras/rpc.py b/netbox/extras/rpc.py index b52dd0310..ae651b162 100644 --- a/netbox/extras/rpc.py +++ b/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': , 'description': , } - 'modules': [ + 'items': [ { 'name': , 'part_id': , @@ -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': [], } diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index a4fbe7170..cc3dd361b 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -46,7 +46,7 @@ - + @@ -55,81 +55,18 @@ - {% for m in modules %} - - - - - - - - - {% for m2 in m.submodules.all %} - - - - - - - - - {% for m3 in m2.submodules.all %} - - - - - - - - - {% for m4 in m3.submodules.all %} - - - - - - - - - {% endfor %} - {% endfor %} - {% endfor %} + {% for item in inventory_items %} + {% with template_name='dcim/inc/inventoryitem.html' indent=0 %} + {% include template_name %} + {% endwith %} {% endfor %}
ModuleName Manufacturer Part Number
{{ m.name }}{% if not m.discovered %}{% endif %}{{ m.manufacturer|default:'' }}{{ m.part_id }}{{ m.serial }} - {% if perms.dcim.change_module %} - - {% endif %} - {% if perms.dcim.delete_module %} - - {% endif %} -
{{ m2.name }}{% if not m2.discovered %}{% endif %}{{ m2.manufacturer|default:'' }}{{ m2.part_id }}{{ m2.serial }} - {% if perms.dcim.change_module %} - - {% endif %} - {% if perms.dcim.delete_module %} - - {% endif %} -
{{ m3.name }}{% if not m3.discovered %}{% endif %}{{ m3.manufacturer|default:'' }}{{ m3.part_id }}{{ m3.serial }} - {% if perms.dcim.change_module %} - - {% endif %} - {% if perms.dcim.delete_module %} - - {% endif %} -
{{ m4.name }}{% if not m4.discovered %}{% endif %}{{ m4.manufacturer|default:'' }}{{ m4.part_id }}{{ m4.serial }} - {% if perms.dcim.change_module %} - - {% endif %} - {% if perms.dcim.delete_module %} - - {% endif %} -
- {% if perms.dcim.add_module %} - + {% if perms.dcim.add_inventoryitem %} + - Add a Module + Add Inventory Item {% endif %} diff --git a/netbox/templates/dcim/inc/inventoryitem.html b/netbox/templates/dcim/inc/inventoryitem.html new file mode 100644 index 000000000..01281c317 --- /dev/null +++ b/netbox/templates/dcim/inc/inventoryitem.html @@ -0,0 +1,20 @@ + + {{ item.name }} + {% if not item.discovered %}{% endif %} + {{ item.manufacturer|default:'' }} + {{ item.part_id }} + {{ item.serial }} + + {% if perms.dcim.change_inventory_item %} + + {% endif %} + {% if perms.dcim.delete_inventory_item %} + + {% endif %} + + +{% for item in item.child_items.all %} + {% with template_name='dcim/inc/inventoryitem.html' indent=indent|add:20 %} + {% include template_name %} + {% endwith %} +{% endfor %} diff --git a/netbox/templates/dcim/inventoryitem_delete.html b/netbox/templates/dcim/inventoryitem_delete.html new file mode 100644 index 000000000..f5d8c648f --- /dev/null +++ b/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 %} +

Are you sure you want to delete this inventory item from {{ inventoryitem.device }}?

+{% endblock %} diff --git a/netbox/templates/dcim/module_delete.html b/netbox/templates/dcim/module_delete.html deleted file mode 100644 index 017464293..000000000 --- a/netbox/templates/dcim/module_delete.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends 'utilities/confirmation_form.html' %} -{% load form_helpers %} - -{% block title %}Delete module {{ module }}?{% endblock %} - -{% block message %} -

Are you sure you want to delete this module from {{ module.device }}?

-{% endblock %}