1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Renamed Module to InventoryItem (prep for #824)

This commit is contained in:
Jeremy Stretch
2017-03-21 12:54:08 -04:00
parent 122526a9d0
commit 22768ff6c6
18 changed files with 192 additions and 200 deletions

View File

@ -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.

View File

@ -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']

View File

@ -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']

View File

@ -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)

View File

@ -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
#

View File

@ -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']

View File

@ -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']

View File

@ -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'),
),
]

View File

@ -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)

View File

@ -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):

View File

@ -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'),
]

View File

@ -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

View File

@ -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!")

View File

@ -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': [],
}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}