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:
@ -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']
|
||||
|
@ -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']
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
#
|
||||
|
@ -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']
|
||||
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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'),
|
||||
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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!")
|
||||
|
@ -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': [],
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
netbox/templates/dcim/inc/inventoryitem.html
Normal file
20
netbox/templates/dcim/inc/inventoryitem.html
Normal 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 %}
|
8
netbox/templates/dcim/inventoryitem_delete.html
Normal file
8
netbox/templates/dcim/inventoryitem_delete.html
Normal 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 %}
|
@ -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 %}
|
Reference in New Issue
Block a user