mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-10 22:02:17 -06:00
Initial push to public repo
This commit is contained in:
1
netbox/dcim/__init__.py
Normal file
1
netbox/dcim/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
default_app_config = 'dcim.apps.IPAMConfig'
|
||||
161
netbox/dcim/admin.py
Normal file
161
netbox/dcim/admin.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Count
|
||||
|
||||
from .models import *
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'facility', 'asn']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackGroup)
|
||||
class RackGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Rack)
|
||||
class RackAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'facility_id', 'site', 'u_height']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
@admin.register(Manufacturer)
|
||||
class ManufacturerAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
class ConsolePortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsolePortTemplate
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPortTemplate
|
||||
|
||||
|
||||
class PowerPortTemplateAdmin(admin.TabularInline):
|
||||
model = PowerPortTemplate
|
||||
|
||||
|
||||
class PowerOutletTemplateAdmin(admin.TabularInline):
|
||||
model = PowerOutletTemplate
|
||||
|
||||
|
||||
class InterfaceTemplateAdmin(admin.TabularInline):
|
||||
model = InterfaceTemplate
|
||||
|
||||
|
||||
@admin.register(DeviceType)
|
||||
class DeviceTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['model'],
|
||||
}
|
||||
inlines = [
|
||||
ConsolePortTemplateAdmin,
|
||||
ConsoleServerPortTemplateAdmin,
|
||||
PowerPortTemplateAdmin,
|
||||
PowerOutletTemplateAdmin,
|
||||
InterfaceTemplateAdmin,
|
||||
]
|
||||
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
|
||||
'power_outlets', 'interfaces']
|
||||
list_filter = ['manufacturer']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return DeviceType.objects.annotate(
|
||||
console_port_count=Count('console_port_templates', distinct=True),
|
||||
cs_port_count=Count('cs_port_templates', distinct=True),
|
||||
power_port_count=Count('power_port_templates', distinct=True),
|
||||
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
||||
interface_count=Count('interface_templates', distinct=True),
|
||||
)
|
||||
|
||||
def console_ports(self, instance):
|
||||
return instance.console_port_count
|
||||
|
||||
def console_server_ports(self, instance):
|
||||
return instance.cs_port_count
|
||||
|
||||
def power_ports(self, instance):
|
||||
return instance.power_port_count
|
||||
|
||||
def power_outlets(self, instance):
|
||||
return instance.power_outlet_count
|
||||
|
||||
def interfaces(self, instance):
|
||||
return instance.interface_count
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
@admin.register(DeviceRole)
|
||||
class DeviceRoleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
@admin.register(Platform)
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'rpc_client']
|
||||
|
||||
|
||||
class ConsolePortAdmin(admin.TabularInline):
|
||||
model = ConsolePort
|
||||
readonly_fields = ['cs_port']
|
||||
|
||||
|
||||
class ConsoleServerPortAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class PowerPortAdmin(admin.TabularInline):
|
||||
model = PowerPort
|
||||
readonly_fields = ['power_outlet']
|
||||
|
||||
|
||||
class PowerOutletAdmin(admin.TabularInline):
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class InterfaceAdmin(admin.TabularInline):
|
||||
model = Interface
|
||||
|
||||
|
||||
class ModuleAdmin(admin.TabularInline):
|
||||
model = Module
|
||||
|
||||
@admin.register(Device)
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
ConsolePortAdmin,
|
||||
ConsoleServerPortAdmin,
|
||||
PowerPortAdmin,
|
||||
PowerOutletAdmin,
|
||||
InterfaceAdmin,
|
||||
ModuleAdmin,
|
||||
]
|
||||
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial']
|
||||
list_filter = ['device_role']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')
|
||||
0
netbox/dcim/api/__init__.py
Normal file
0
netbox/dcim/api/__init__.py
Normal file
6
netbox/dcim/api/exceptions.py
Normal file
6
netbox/dcim/api/exceptions.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class MissingFilterException(APIException):
|
||||
status_code = 400
|
||||
default_detail = "One or more required filters is missing from the request."
|
||||
300
netbox/dcim/api/serializers.py
Normal file
300
netbox/dcim/api/serializers.py
Normal file
@@ -0,0 +1,300 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from dcim.models import Site, Rack, RackGroup, Manufacturer, DeviceType, DeviceRole, Platform, Device, ConsolePort,\
|
||||
ConsoleServerPort, PowerPort, PowerOutlet, Interface, InterfaceConnection, RACK_FACE_FRONT, RACK_FACE_REAR
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
||||
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||
|
||||
|
||||
class SiteNestedSerializer(SiteSerializer):
|
||||
|
||||
class Meta(SiteSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'name', 'slug', 'site']
|
||||
|
||||
|
||||
class RackGroupNestedSerializer(SiteSerializer):
|
||||
|
||||
class Meta(SiteSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
|
||||
class RackSerializer(serializers.ModelSerializer):
|
||||
display_name = serializers.SerializerMethodField()
|
||||
site = SiteNestedSerializer()
|
||||
group = RackGroupNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
|
||||
|
||||
def get_display_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class RackNestedSerializer(RackSerializer):
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name']
|
||||
|
||||
|
||||
class RackDetailSerializer(RackSerializer):
|
||||
front_units = serializers.SerializerMethodField()
|
||||
rear_units = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
|
||||
'rear_units']
|
||||
|
||||
def get_front_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
||||
for u in units:
|
||||
u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None
|
||||
return units
|
||||
|
||||
def get_rear_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_REAR)
|
||||
for u in units:
|
||||
u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None
|
||||
return units
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ManufacturerNestedSerializer(ManufacturerSerializer):
|
||||
|
||||
class Meta(ManufacturerSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeSerializer(serializers.ModelSerializer):
|
||||
manufacturer = ManufacturerNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device']
|
||||
|
||||
|
||||
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
|
||||
|
||||
class Meta(DeviceTypeSerializer.Meta):
|
||||
fields = ['id', 'manufacturer', 'model', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class DeviceRoleNestedSerializer(DeviceRoleSerializer):
|
||||
|
||||
class Meta(DeviceRoleSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'rpc_client']
|
||||
|
||||
|
||||
class PlatformNestedSerializer(PlatformSerializer):
|
||||
|
||||
class Meta(PlatformSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.IPAddressNestedSerializer due to circular dependency
|
||||
class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'family', 'address']
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
device_type = DeviceTypeNestedSerializer()
|
||||
device_role = DeviceRoleNestedSerializer()
|
||||
platform = PlatformNestedSerializer()
|
||||
rack = RackNestedSerializer()
|
||||
primary_ip = DeviceIPAddressNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
||||
'face', 'status', 'primary_ip', 'ro_snmp', 'comments']
|
||||
|
||||
|
||||
class DeviceNestedSerializer(DeviceSerializer):
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
model = Device
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'device', 'name', 'connected_console']
|
||||
|
||||
|
||||
class ConsoleServerPortNestedSerializer(ConsoleServerPortSerializer):
|
||||
|
||||
class Meta(ConsoleServerPortSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
cs_port = ConsoleServerPortNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||
|
||||
|
||||
class ConsolePortNestedSerializer(ConsolePortSerializer):
|
||||
|
||||
class Meta(ConsolePortSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'device', 'name', 'connected_port']
|
||||
|
||||
|
||||
class PowerOutletNestedSerializer(PowerOutletSerializer):
|
||||
|
||||
class Meta(PowerOutletSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
power_outlet = PowerOutletNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||
|
||||
|
||||
class PowerPortNestedSerializer(PowerPortSerializer):
|
||||
|
||||
class Meta(PowerPortSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected']
|
||||
|
||||
|
||||
class InterfaceNestedSerializer(InterfaceSerializer):
|
||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
class InterfaceDetailSerializer(InterfaceSerializer):
|
||||
connected_interface = InterfaceSerializer(source='get_connected_interface')
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected',
|
||||
'connected_interface']
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
||||
581
netbox/dcim/api/tests.py
Normal file
581
netbox/dcim/api/tests.py
Normal file
@@ -0,0 +1,581 @@
|
||||
import json
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
class SiteTest(APITestCase):
|
||||
|
||||
fixtures = [
|
||||
'dcim',
|
||||
'ipam',
|
||||
'extras',
|
||||
]
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'facility',
|
||||
'asn',
|
||||
'physical_address',
|
||||
'shipping_address',
|
||||
'comments',
|
||||
'count_prefixes',
|
||||
'count_vlans',
|
||||
'count_racks',
|
||||
'count_devices',
|
||||
'count_circuits'
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'slug'
|
||||
]
|
||||
|
||||
rack_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'u_height',
|
||||
'comments'
|
||||
]
|
||||
|
||||
graph_fields = [
|
||||
'name',
|
||||
'embed_url',
|
||||
'link',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/sites/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/sites/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_site_list_rack(self, endpoint='/api/dcim/sites/1/racks/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in json.loads(response.content):
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.rack_fields),
|
||||
)
|
||||
# Check Nested Serializer.
|
||||
self.assertEqual(
|
||||
sorted(i.get('site').keys()),
|
||||
sorted(self.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_site_list_graphs(self, endpoint='/api/dcim/sites/1/graphs/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in json.loads(response.content):
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.graph_fields),
|
||||
)
|
||||
|
||||
|
||||
class RackTest(APITestCase):
|
||||
fixtures = [
|
||||
'dcim',
|
||||
'ipam'
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name'
|
||||
]
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'u_height',
|
||||
'comments'
|
||||
]
|
||||
|
||||
detail_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'u_height',
|
||||
'comments',
|
||||
'front_units',
|
||||
'rear_units'
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/racks/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('site').keys()),
|
||||
sorted(SiteTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/racks/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.detail_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('site').keys()),
|
||||
sorted(SiteTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class ManufacturersTest(APITestCase):
|
||||
|
||||
fixtures = [
|
||||
'dcim',
|
||||
'ipam'
|
||||
]
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
]
|
||||
|
||||
nested_fields = standard_fields
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/manufacturers/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/manufacturers/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'manufacturer',
|
||||
'model',
|
||||
'slug',
|
||||
'u_height',
|
||||
'is_console_server',
|
||||
'is_pdu',
|
||||
'is_network_device'
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
'id',
|
||||
'manufacturer',
|
||||
'model',
|
||||
'slug'
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/device-types/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_detail_list(self, endpoint='/api/dcim/device-types/1/'):
|
||||
# TODO: details returns list view.
|
||||
# response = self.client.get(endpoint)
|
||||
# content = json.loads(response.content)
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(
|
||||
# sorted(content.keys()),
|
||||
# sorted(self.standard_fields),
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# sorted(content.get('manufacturer').keys()),
|
||||
# sorted(ManufacturersTest.nested_fields),
|
||||
# )
|
||||
pass
|
||||
|
||||
|
||||
class DeviceRolesTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
nested_fields = ['id', 'name', 'slug']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/device-roles/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/device-roles/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class PlatformsTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'name', 'slug', 'rpc_client']
|
||||
|
||||
nested_fields = ['id', 'name', 'slug']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/platforms/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/platforms/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class DeviceTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'display_name',
|
||||
'device_type',
|
||||
'device_role',
|
||||
'platform',
|
||||
'serial',
|
||||
'rack',
|
||||
'position',
|
||||
'face',
|
||||
'status',
|
||||
'primary_ip',
|
||||
'ro_snmp',
|
||||
'comments',
|
||||
]
|
||||
|
||||
nested_fields = ['id', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for device in content:
|
||||
self.assertEqual(
|
||||
sorted(device.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(device.get('device_type')),
|
||||
sorted(DeviceTypeTest.nested_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(device.get('device_role')),
|
||||
sorted(DeviceRolesTest.nested_fields),
|
||||
)
|
||||
if device.get('platform'):
|
||||
self.assertEqual(
|
||||
sorted(device.get('platform')),
|
||||
sorted(PlatformsTest.nested_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(device.get('rack')),
|
||||
sorted(RackTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/devices/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class ConsoleServerPortsTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'connected_console']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/9/console-server-ports/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for console_port in content:
|
||||
self.assertEqual(
|
||||
sorted(console_port.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(console_port.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortsTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/1/console-ports/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for console_port in content:
|
||||
self.assertEqual(
|
||||
sorted(console_port.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(console_port.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(console_port.get('cs_port')),
|
||||
sorted(ConsoleServerPortsTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/console-ports/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class PowerPortsTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/1/power-ports/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/power-ports/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletsTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'connected_port']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/11/power-outlets/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam', 'extras']
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'device',
|
||||
'name',
|
||||
'form_factor',
|
||||
'mgmt_only',
|
||||
'description',
|
||||
'is_connected'
|
||||
]
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
detail_fields = [
|
||||
'id',
|
||||
'device',
|
||||
'name',
|
||||
'form_factor',
|
||||
'mgmt_only',
|
||||
'description',
|
||||
'is_connected',
|
||||
'connected_interface'
|
||||
]
|
||||
|
||||
connection_fields = [
|
||||
'id',
|
||||
'interface_a',
|
||||
'interface_b',
|
||||
'connection_status',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/1/interfaces/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/interfaces/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.detail_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_graph_list(self, endpoint='/api/dcim/interfaces/1/graphs/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(SiteTest.graph_fields),
|
||||
)
|
||||
|
||||
def test_get_interface_connections(self, endpoint='/api/dcim/interface-connections/4/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.connection_fields),
|
||||
)
|
||||
|
||||
|
||||
class RelatedConnectionsTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = [
|
||||
'device',
|
||||
'console-ports',
|
||||
'power-ports',
|
||||
'interfaces',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint=(
|
||||
'/api/dcim/related-connections/'
|
||||
'?peer-device=test1-edge1&peer-interface=xe-0/0/3')):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
66
netbox/dcim/api/urls.py
Normal file
66
netbox/dcim/api/urls.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.api.views import GraphListView
|
||||
|
||||
from .views import *
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
|
||||
url(r'^sites/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'),
|
||||
url(r'^sites/(?P<site>\d+)/racks/$', RackListView.as_view(), name='site_racks'),
|
||||
|
||||
# Rack groups
|
||||
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
|
||||
url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
|
||||
|
||||
# Device types
|
||||
url(r'^device-types/$', DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
url(r'^device-types/(?P<pk>\d+)/$', DeviceTypeDetailView.as_view(), name='devicetype_detail'),
|
||||
|
||||
# Device roles
|
||||
url(r'^device-roles/$', DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
url(r'^device-roles/(?P<pk>\d+)/$', DeviceRoleDetailView.as_view(), name='devicerole_detail'),
|
||||
|
||||
# Platforms
|
||||
url(r'^platforms/$', PlatformListView.as_view(), name='platform_list'),
|
||||
url(r'^platforms/(?P<pk>\d+)/$', PlatformDetailView.as_view(), name='platform_detail'),
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', DeviceListView.as_view(), name='device_list'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', DeviceDetailView.as_view(), name='device_detail'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/$', ConsolePortListView.as_view(), name='device_consoleports'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/$', ConsoleServerPortListView.as_view(), name='device_consoleserverports'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
|
||||
|
||||
# Console ports
|
||||
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
|
||||
|
||||
# Power ports
|
||||
url(r'^power-ports/(?P<pk>\d+)/$', PowerPortView.as_view(), name='powerport'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE}, name='interface_graphs'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection'),
|
||||
|
||||
# Miscellaneous
|
||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||
|
||||
]
|
||||
438
netbox/dcim/api/views.py
Normal file
438
netbox/dcim/api/views.py
Normal file
@@ -0,0 +1,438 @@
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim.models import Site, Rack, RackGroup, Manufacturer, DeviceType, DeviceRole, Platform, Device, ConsolePort, \
|
||||
ConsoleServerPort, PowerPort, PowerOutlet, Interface, InterfaceConnection, IFACE_FF_VIRTUAL
|
||||
from dcim.filters import RackGroupFilter, RackFilter, DeviceTypeFilter, DeviceFilter, InterfaceFilter
|
||||
from .exceptions import MissingFilterException
|
||||
from .serializers import SiteSerializer, RackGroupSerializer, RackSerializer, RackDetailSerializer, \
|
||||
ManufacturerSerializer, DeviceTypeSerializer, DeviceRoleSerializer, PlatformSerializer, DeviceSerializer, \
|
||||
DeviceNestedSerializer, ConsolePortSerializer, ConsoleServerPortSerializer, PowerPortSerializer, \
|
||||
PowerOutletSerializer, InterfaceSerializer, InterfaceDetailSerializer, InterfaceConnectionSerializer
|
||||
from extras.api.renderers import BINDZoneRenderer
|
||||
from utilities.api import ServiceUnavailable
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteListView(generics.ListAPIView):
|
||||
"""
|
||||
List all sites
|
||||
"""
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = SiteSerializer
|
||||
|
||||
|
||||
class SiteDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single site
|
||||
"""
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = SiteSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack groups
|
||||
"""
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = RackGroupSerializer
|
||||
filter_class = RackGroupFilter
|
||||
|
||||
|
||||
class RackGroupDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack group
|
||||
"""
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = RackGroupSerializer
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackListView(generics.ListAPIView):
|
||||
"""
|
||||
List racks (filterable)
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site')
|
||||
serializer_class = RackSerializer
|
||||
filter_class = RackFilter
|
||||
|
||||
|
||||
class RackDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site')
|
||||
serializer_class = RackDetailSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack units
|
||||
#
|
||||
|
||||
class RackUnitListView(APIView):
|
||||
"""
|
||||
List rack units (by rack)
|
||||
"""
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
face = request.GET.get('face', 0)
|
||||
elevation = rack.get_rack_units(face)
|
||||
|
||||
# Serialize Devices within the rack elevation
|
||||
for u in elevation:
|
||||
if u['device']:
|
||||
u['device'] = DeviceNestedSerializer(instance=u['device']).data
|
||||
|
||||
return Response(elevation)
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerListView(generics.ListAPIView):
|
||||
"""
|
||||
List all hardware manufacturers
|
||||
"""
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = ManufacturerSerializer
|
||||
|
||||
|
||||
class ManufacturerDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single hardware manufacturers
|
||||
"""
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = ManufacturerSerializer
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
#
|
||||
|
||||
class DeviceTypeListView(generics.ListAPIView):
|
||||
"""
|
||||
List device types (filterable)
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
serializer_class = DeviceTypeSerializer
|
||||
filter_class = DeviceTypeFilter
|
||||
|
||||
|
||||
class DeviceTypeDetailView(generics.ListAPIView):
|
||||
"""
|
||||
Retrieve a single device type
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
serializer_class = DeviceTypeSerializer
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all device roles
|
||||
"""
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = DeviceRoleSerializer
|
||||
|
||||
|
||||
class DeviceRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device role
|
||||
"""
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = DeviceRoleSerializer
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformListView(generics.ListAPIView):
|
||||
"""
|
||||
List all platforms
|
||||
"""
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = PlatformSerializer
|
||||
|
||||
|
||||
class PlatformDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single platform
|
||||
"""
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = PlatformSerializer
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceListView(generics.ListAPIView):
|
||||
"""
|
||||
List devices (filterable)
|
||||
"""
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
|
||||
.prefetch_related('primary_ip__nat_outside')
|
||||
serializer_class = DeviceSerializer
|
||||
filter_class = DeviceFilter
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer]
|
||||
|
||||
|
||||
class DeviceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device
|
||||
"""
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = DeviceSerializer
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortListView(generics.ListAPIView):
|
||||
"""
|
||||
List console ports (by device)
|
||||
"""
|
||||
serializer_class = ConsolePortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return ConsolePort.objects.filter(device=device).select_related('cs_port')
|
||||
|
||||
|
||||
class ConsolePortView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = ConsolePortSerializer
|
||||
queryset = ConsolePort.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortListView(generics.ListAPIView):
|
||||
"""
|
||||
List console server ports (by device)
|
||||
"""
|
||||
serializer_class = ConsoleServerPortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortListView(generics.ListAPIView):
|
||||
"""
|
||||
List power ports (by device)
|
||||
"""
|
||||
serializer_class = PowerPortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return PowerPort.objects.filter(device=device).select_related('power_outlet')
|
||||
|
||||
|
||||
class PowerPortView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = PowerPortSerializer
|
||||
queryset = PowerPort.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletListView(generics.ListAPIView):
|
||||
"""
|
||||
List power outlets (by device)
|
||||
"""
|
||||
serializer_class = PowerOutletSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceListView(generics.ListAPIView):
|
||||
"""
|
||||
List interfaces (by device)
|
||||
"""
|
||||
serializer_class = InterfaceSerializer
|
||||
filter_class = InterfaceFilter
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
queryset = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b')
|
||||
|
||||
# Filter by type (physical or virtual)
|
||||
iface_type = self.request.query_params.get('type')
|
||||
if iface_type == 'physical':
|
||||
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
|
||||
elif iface_type == 'virtual':
|
||||
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
|
||||
elif iface_type is not None:
|
||||
queryset = queryset.empty()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class InterfaceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single interface
|
||||
"""
|
||||
queryset = Interface.objects.select_related('device')
|
||||
serializer_class = InterfaceDetailSerializer
|
||||
|
||||
|
||||
class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = InterfaceConnectionSerializer
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Live queries
|
||||
#
|
||||
|
||||
class LLDPNeighborsView(APIView):
|
||||
"""
|
||||
Retrieve live LLDP neighbors of a device
|
||||
"""
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
if not device.primary_ip:
|
||||
raise ServiceUnavailable(detail="No IP configured for this device.")
|
||||
hostname = str(device.primary_ip.address.ip)
|
||||
|
||||
RPC = device.get_rpc_client()
|
||||
if not RPC:
|
||||
raise ServiceUnavailable(detail="No RPC client available for this platform ({}).".format(device.platform))
|
||||
|
||||
# Connect to device and retrieve inventory info
|
||||
try:
|
||||
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
|
||||
lldp_neighbors = rpc_client.get_lldp_neighbors()
|
||||
except:
|
||||
raise ServiceUnavailable(detail="Error connecting to the remote device.")
|
||||
|
||||
return Response(lldp_neighbors)
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
class RelatedConnectionsView(APIView):
|
||||
"""
|
||||
Retrieve all connections related to a given console/power/interface connection
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
|
||||
peer_device = request.GET.get('peer-device')
|
||||
peer_interface = request.GET.get('peer-interface')
|
||||
|
||||
# Search by interface
|
||||
if peer_device and peer_interface:
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
try:
|
||||
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
|
||||
except Interface.DoesNotExist:
|
||||
raise Http404()
|
||||
local_iface = peer_iface.get_connected_interface()
|
||||
if local_iface:
|
||||
device = local_iface.device
|
||||
else:
|
||||
return Response()
|
||||
|
||||
else:
|
||||
raise MissingFilterException(detail='Must specify search parameters (peer-device and peer-interface).')
|
||||
|
||||
# Initialize response skeleton
|
||||
response = dict()
|
||||
response['device'] = DeviceSerializer(device).data
|
||||
response['console-ports'] = []
|
||||
response['power-ports'] = []
|
||||
response['interfaces'] = []
|
||||
|
||||
# Build console connections
|
||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
||||
for cp in console_ports:
|
||||
cp_info = dict()
|
||||
cp_info['name'] = cp.name
|
||||
if cp.cs_port:
|
||||
cp_info['console-server'] = cp.cs_port.device.name
|
||||
cp_info['port'] = cp.cs_port.name
|
||||
else:
|
||||
cp_info['console-server'] = None
|
||||
cp_info['port'] = None
|
||||
response['console-ports'].append(cp_info)
|
||||
|
||||
# Build power connections
|
||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
||||
for pp in power_ports:
|
||||
pp_info = dict()
|
||||
pp_info['name'] = pp.name
|
||||
if pp.power_outlet:
|
||||
pp_info['pdu'] = pp.power_outlet.device.name
|
||||
pp_info['outlet'] = pp.power_outlet.name
|
||||
else:
|
||||
pp_info['pdu'] = None
|
||||
pp_info['outlet'] = None
|
||||
response['power-ports'].append(pp_info)
|
||||
|
||||
# Built interface connections
|
||||
interfaces = Interface.objects.filter(device=device)
|
||||
for iface in interfaces:
|
||||
iface_info = dict()
|
||||
iface_info['name'] = iface.name
|
||||
peer_interface = iface.get_connected_interface()
|
||||
if peer_interface:
|
||||
iface_info['device'] = peer_interface.device.name
|
||||
iface_info['interface'] = peer_interface.name
|
||||
else:
|
||||
iface_info['device'] = None
|
||||
iface_info['interface'] = None
|
||||
response['interfaces'].append(iface_info)
|
||||
|
||||
return Response(response)
|
||||
6
netbox/dcim/apps.py
Normal file
6
netbox/dcim/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IPAMConfig(AppConfig):
|
||||
name = "dcim"
|
||||
verbose_name = "DCIM"
|
||||
317
netbox/dcim/filters.py
Normal file
317
netbox/dcim/filters.py
Normal file
@@ -0,0 +1,317 @@
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import Site, RackGroup, Rack, Manufacturer, DeviceType, DeviceRole, Device, ConsolePort, \
|
||||
ConsoleServerPort, Platform, PowerPort, PowerOutlet, Interface, InterfaceConnection
|
||||
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'site']
|
||||
|
||||
|
||||
class RackFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['q', 'site_id', 'site', 'u_height']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(facility_id__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeFilter(django_filters.FilterSet):
|
||||
manufacturer_id = django_filters.ModelChoiceFilter(
|
||||
name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelChoiceFilter(
|
||||
name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'u_height', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
|
||||
|
||||
class DeviceFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
device_type = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type (ID)',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
model = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Device model (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
is_console_server = django_filters.BooleanFilter(
|
||||
name='device_type__is_console_server',
|
||||
label='Is a console server',
|
||||
)
|
||||
is_pdu = django_filters.BooleanFilter(
|
||||
name='device_type__is_pdu',
|
||||
label='Is a PDU',
|
||||
)
|
||||
is_network_device = django_filters.BooleanFilter(
|
||||
name='device_type__is_network_device',
|
||||
label='Is a network device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['q', 'name', 'site_id', 'site', 'rack_id', 'role_id', 'role', 'device_type', 'manufacturer_id',
|
||||
'manufacturer', 'model', 'platform_id', 'platform', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(serial__icontains=value) |
|
||||
Q(modules__serial__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class ConsolePortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class PowerPortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class PowerOutletFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class InterfaceFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__rack__site__slug=value)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__rack__site__slug=value)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__rack__site__slug=value) |
|
||||
Q(interface_b__device__rack__site__slug=value)
|
||||
)
|
||||
1794
netbox/dcim/fixtures/dcim.yaml
Normal file
1794
netbox/dcim/fixtures/dcim.yaml
Normal file
File diff suppressed because it is too large
Load Diff
953
netbox/dcim/forms.py
Normal file
953
netbox/dcim/forms.py
Normal file
@@ -0,0 +1,953 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from utilities.forms import BootstrapMixin, SmallTextarea, SelectWithDisabled, ConfirmationForm, APISelect, \
|
||||
Livesearch, CSVDataField, CommentField, BulkImportForm, FlexibleModelChoiceField, ExpandableNameField
|
||||
|
||||
from .models import Site, Rack, RackGroup, Device, Manufacturer, DeviceType, DeviceRole, Platform, ConsolePort, \
|
||||
ConsoleServerPort, PowerPort, PowerOutlet, Interface, InterfaceConnection, CONNECTION_STATUS_CHOICES, \
|
||||
CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, IFACE_FF_VIRTUAL, STATUS_CHOICES
|
||||
|
||||
|
||||
BULK_STATUS_CHOICES = [
|
||||
['', '---------'],
|
||||
]
|
||||
BULK_STATUS_CHOICES += STATUS_CHOICES
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
|
||||
|
||||
def get_device_by_name_or_pk(name):
|
||||
"""
|
||||
Attempt to retrieve a device by either its name or primary key ('{pk}').
|
||||
"""
|
||||
if re.match(DEVICE_BY_PK_RE, name):
|
||||
pk = name.strip('{}')
|
||||
device = Device.objects.get(pk=pk)
|
||||
else:
|
||||
device = Device.objects.get(name=name)
|
||||
return device
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteForm(forms.ModelForm, BootstrapMixin):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the site",
|
||||
'slug': "URL-friendly unique shorthand (e.g. 'nyc3' for NYC3)",
|
||||
'facility': "Data center provider and facility (e.g. Equinix NY7)",
|
||||
'asn': "BGP autonomous system number",
|
||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||
'shipping_address': "If different from the physical address"
|
||||
}
|
||||
|
||||
|
||||
class SiteFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'facility', 'asn']
|
||||
|
||||
|
||||
class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackForm(forms.ModelForm, BootstrapMixin):
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
|
||||
api_url='/api/dcim/rack-groups/?site_id={{site}}',
|
||||
))
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
'facility_id': "The unique rack ID assigned by the facility",
|
||||
'u_height': "Height in rack units",
|
||||
}
|
||||
widgets = {
|
||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(RackForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit rack group choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['group'].choices = []
|
||||
|
||||
|
||||
class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Group not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height']
|
||||
|
||||
def clean(self):
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
group = self.cleaned_data.get('group')
|
||||
|
||||
# Validate device type
|
||||
if site and group:
|
||||
try:
|
||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
||||
except RackGroup.DoesNotExist:
|
||||
self.add_error('group', "Invalid rack group ({})".format(group))
|
||||
|
||||
|
||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
||||
|
||||
|
||||
class RackBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
|
||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class RackBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def rack_site_choices():
|
||||
site_choices = Site.objects.annotate(rack_count=Count('racks'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
|
||||
|
||||
def rack_group_choices():
|
||||
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||
return [(g.slug, '{} > {} ({})'.format(g.site.name, g.name, g.rack_count)) for g in group_choices]
|
||||
|
||||
|
||||
class RackFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
))
|
||||
position = forms.TypedChoiceField(required=False, empty_value=None, widget=APISelect(
|
||||
api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
|
||||
disabled_indicator='device',
|
||||
))
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||
widget=forms.Select(attrs={'filter-for': 'device_type'}))
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
|
||||
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
||||
display_field='model'
|
||||
))
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
||||
'platform', 'primary_ip', 'ro_snmp', 'comments']
|
||||
help_texts = {
|
||||
'device_role': "The function this device serves",
|
||||
'serial': "Chassis serial number",
|
||||
'ro_snmp': "Read-only SNMP string",
|
||||
}
|
||||
widgets = {
|
||||
'face': forms.Select(attrs={'filter-for': 'position'}),
|
||||
'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(DeviceForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
|
||||
# Initialize helper selections
|
||||
self.initial['site'] = self.instance.rack.site
|
||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||
|
||||
# Compile list of IPs assigned to this device
|
||||
primary_ip_choices = []
|
||||
interface_ips = IPAddress.objects.filter(interface__device=self.instance)
|
||||
primary_ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
nat_ips = IPAddress.objects.filter(nat_inside__interface__device=self.instance)\
|
||||
.select_related('nat_inside__interface')
|
||||
primary_ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip'].choices = [(None, '---------')] + primary_ip_choices
|
||||
|
||||
else:
|
||||
|
||||
# An object that doesn't exist yet can't have any IPs assigned to it
|
||||
self.fields['primary_ip'].choices = []
|
||||
self.fields['primary_ip'].widget.attrs['readonly'] = True
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound:
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Rack position
|
||||
try:
|
||||
if self.is_bound and self.data.get('rack') and self.data.get('face') is not None:
|
||||
position_choices = Rack.objects.get(pk=self.data['rack']).get_rack_units(face=self.data.get('face'))
|
||||
elif self.initial.get('rack') and self.initial.get('face') is not None:
|
||||
position_choices = Rack.objects.get(pk=self.initial['rack']).get_rack_units(face=self.initial.get('face'))
|
||||
else:
|
||||
position_choices = []
|
||||
except Rack.DoesNotExist:
|
||||
position_choices = []
|
||||
self.fields['position'].choices = [('', '---------')] + [
|
||||
(p['id'], {
|
||||
'label': p['name'],
|
||||
'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
|
||||
}) for p in position_choices
|
||||
]
|
||||
|
||||
# Limit device_type choices
|
||||
if self.is_bound:
|
||||
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
|
||||
.select_related('manufacturer')
|
||||
elif self.initial.get('manufacturer'):
|
||||
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
|
||||
.select_related('manufacturer')
|
||||
else:
|
||||
self.fields['device_type'].choices = []
|
||||
|
||||
|
||||
class DeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
||||
model_name = forms.CharField()
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'})
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
})
|
||||
rack_name = forms.CharField()
|
||||
face = forms.ChoiceField(choices=[('front', 'Front'), ('rear', 'Rear')])
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
||||
'position', 'face']
|
||||
|
||||
def clean(self):
|
||||
|
||||
manufacturer = self.cleaned_data.get('manufacturer')
|
||||
model_name = self.cleaned_data.get('model_name')
|
||||
site = self.cleaned_data.get('site')
|
||||
rack_name = self.cleaned_data.get('rack_name')
|
||||
|
||||
# Validate device type
|
||||
if manufacturer and model_name:
|
||||
try:
|
||||
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
||||
except DeviceType.DoesNotExist:
|
||||
self.add_error('model_name', "Invalid device type ({})".format(model_name))
|
||||
|
||||
# Validate rack
|
||||
if site and rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
|
||||
|
||||
def clean_face(self):
|
||||
face = self.cleaned_data['face']
|
||||
if face.lower() == 'front':
|
||||
return 0
|
||||
if face.lower() == 'rear':
|
||||
return 1
|
||||
raise forms.ValidationError("Invalid rack face ({})".format(face))
|
||||
|
||||
|
||||
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||
|
||||
|
||||
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
|
||||
platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
|
||||
status = forms.ChoiceField(choices=BULK_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
ro_snmp = forms.CharField(max_length=50, required=False, label='SNMP (RO)')
|
||||
|
||||
|
||||
class DeviceBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def device_site_choices():
|
||||
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
|
||||
|
||||
def device_role_choices():
|
||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||
|
||||
|
||||
def device_type_choices():
|
||||
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
|
||||
return [(t.slug, '{} ({})'.format(t, t.device_count)) for t in type_choices]
|
||||
|
||||
|
||||
def device_platform_choices():
|
||||
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
|
||||
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||
|
||||
|
||||
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
type = forms.MultipleChoiceField(required=False, choices=device_type_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortCreateForm(forms.Form, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleConnectionCSVForm(forms.Form):
|
||||
console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||
to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Console server not found'})
|
||||
cs_port = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
console_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate console server port
|
||||
if self.cleaned_data.get('console_server'):
|
||||
try:
|
||||
cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
|
||||
name=self.cleaned_data['cs_port'])
|
||||
if ConsolePort.objects.filter(cs_port=cs_port):
|
||||
raise forms.ValidationError("Console server port is already occupied (by {} {})"
|
||||
.format(cs_port.connected_console.device, cs_port.connected_console))
|
||||
except ConsoleServerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console server port ({} {})"
|
||||
.format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
|
||||
|
||||
# Validate console port
|
||||
if self.cleaned_data.get('device'):
|
||||
try:
|
||||
console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['console_port'])
|
||||
if console_port.cs_port:
|
||||
raise forms.ValidationError("Console port is already connected (to {} {})"
|
||||
.format(console_port.cs_port.device, console_port.cs_port))
|
||||
except ConsolePort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console port ({} {})"
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
||||
|
||||
|
||||
class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
connection_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
|
||||
name=form.cleaned_data['console_port'])
|
||||
console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
|
||||
name=form.cleaned_data['cs_port'])
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
console_port.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
console_port.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(console_port)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
||||
attrs={'filter-for': 'cs_port'}))
|
||||
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
||||
)
|
||||
cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
|
||||
disabled_indicator='connected_console'))
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
|
||||
labels = {
|
||||
'cs_port': 'Port',
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if not self.instance.pk:
|
||||
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
|
||||
self.fields['cs_port'].required = True
|
||||
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
||||
|
||||
# Initialize console server choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
|
||||
else:
|
||||
self.fields['console_server'].choices = []
|
||||
|
||||
# Initialize CS port choices
|
||||
if self.is_bound:
|
||||
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.data['console_server'])
|
||||
elif self.initial.get('console_server', None):
|
||||
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.initial['console_server'])
|
||||
else:
|
||||
self.fields['cs_port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(forms.Form, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
|
||||
disabled_indicator='cs_port'))
|
||||
connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
|
||||
widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
|
||||
|
||||
class Meta:
|
||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
labels = {
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, consoleserverport, *args, **kwargs):
|
||||
|
||||
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
|
||||
|
||||
# Initialize device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Initialize port choices
|
||||
if self.is_bound:
|
||||
self.fields['port'].queryset = ConsolePort.objects.filter(device__pk=self.data['device'])
|
||||
elif self.initial.get('device', None):
|
||||
self.fields['port'].queryset = ConsolePort.objects.filter(device_pk=self.initial['device'])
|
||||
else:
|
||||
self.fields['port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerPortCreateForm(forms.Form, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerConnectionCSVForm(forms.Form):
|
||||
pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'PDU not found.'})
|
||||
power_outlet = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
power_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate power outlet
|
||||
if self.cleaned_data.get('pdu'):
|
||||
try:
|
||||
power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
|
||||
name=self.cleaned_data['power_outlet'])
|
||||
if PowerPort.objects.filter(power_outlet=power_outlet):
|
||||
raise forms.ValidationError("Power outlet is already occupied (by {} {})"
|
||||
.format(power_outlet.connected_console.device,
|
||||
power_outlet.connected_console))
|
||||
except PowerOutlet.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid PDU port ({} {})"
|
||||
.format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
|
||||
|
||||
# Validate power port
|
||||
if self.cleaned_data.get('device'):
|
||||
try:
|
||||
power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['power_port'])
|
||||
if power_port.power_outlet:
|
||||
raise forms.ValidationError("Power port is already connected (to {} {})"
|
||||
.format(power_port.power_outlet.device, power_port.power_outlet))
|
||||
except PowerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid power port ({} {})"
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
||||
|
||||
|
||||
class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
connection_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
|
||||
name=form.cleaned_data['power_port'])
|
||||
power_port.cs_port = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
|
||||
name=form.cleaned_data['power_outlet'])
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
power_port.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
power_port.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(power_port)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
||||
attrs={'filter-for': 'power_outlet'}))
|
||||
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
||||
)
|
||||
power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
|
||||
disabled_indicator='connected_port'))
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
|
||||
labels = {
|
||||
'power_outlet': 'Outlet',
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if not self.instance.pk:
|
||||
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
|
||||
self.fields['power_outlet'].required = True
|
||||
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
||||
|
||||
# Initialize PDU choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
|
||||
else:
|
||||
self.fields['pdu'].choices = []
|
||||
|
||||
# Initialize power outlet choices
|
||||
if self.is_bound:
|
||||
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.data['pdu'])
|
||||
elif self.initial.get('pdu', None):
|
||||
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.initial['pdu'])
|
||||
else:
|
||||
self.fields['power_outlet'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletCreateForm(forms.Form, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
|
||||
disabled_indicator='power_outlet'))
|
||||
connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
|
||||
widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
|
||||
|
||||
class Meta:
|
||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
labels = {
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, poweroutlet, *args, **kwargs):
|
||||
|
||||
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
|
||||
|
||||
# Initialize device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Initialize port choices
|
||||
if self.is_bound:
|
||||
self.fields['port'].queryset = PowerPort.objects.filter(device__pk=self.data['device'])
|
||||
elif self.initial.get('device', None):
|
||||
self.fields['port'].queryset = PowerPort.objects.filter(device_pk=self.initial['device'])
|
||||
else:
|
||||
self.fields['port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device', 'name', 'form_factor', 'mgmt_only', 'description']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
|
||||
rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device_b'}))
|
||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||
attrs={'filter-for': 'interface_b'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
||||
)
|
||||
interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'))
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['interface_a', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
|
||||
|
||||
def __init__(self, device_a, *args, **kwargs):
|
||||
|
||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack_b'].queryset = Rack.objects.filter(site=device_a.rack.site)
|
||||
|
||||
# Initialize interface A choices
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
|
||||
.select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
self.fields['interface_a'].choices = [
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
||||
]
|
||||
|
||||
# Initialize device_b choices if rack_b is set
|
||||
if self.is_bound and self.data.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
|
||||
elif self.initial.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
|
||||
else:
|
||||
self.fields['device_b'].choices = []
|
||||
|
||||
# Initialize interface_b choices if device_b is set
|
||||
if self.is_bound:
|
||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
elif self.initial.get('device_b'):
|
||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
else:
|
||||
device_b_interfaces = []
|
||||
self.fields['interface_b'].choices = [
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
|
||||
]
|
||||
|
||||
|
||||
class InterfaceConnectionCSVForm(forms.Form):
|
||||
device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device A not found.'})
|
||||
interface_a = forms.CharField()
|
||||
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device B not found.'})
|
||||
interface_b = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate interface A
|
||||
if self.cleaned_data.get('device_a'):
|
||||
try:
|
||||
interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
|
||||
name=self.cleaned_data['interface_a'])
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})"
|
||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
||||
try:
|
||||
InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
|
||||
raise forms.ValidationError("{} {} is already connected"
|
||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Validate interface B
|
||||
if self.cleaned_data.get('device_b'):
|
||||
try:
|
||||
interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
|
||||
name=self.cleaned_data['interface_b'])
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})"
|
||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
||||
try:
|
||||
InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
|
||||
raise forms.ValidationError("{} {} is already connected"
|
||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
connection_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
|
||||
name=form.cleaned_data['interface_a'])
|
||||
interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
|
||||
name=form.cleaned_data['interface_b'])
|
||||
connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
connection.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
connection.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(connection)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
# Used for HTTP redirect upon successful deletion
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'interface', 'set_as_primary']
|
||||
help_texts = {
|
||||
'address': 'IPv4 or IPv6 address (with mask)'
|
||||
}
|
||||
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
self.fields['interface'].queryset = device.interfaces.all()
|
||||
self.fields['interface'].required = True
|
||||
|
||||
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary
|
||||
if not IPAddress.objects.filter(interface__device=device).count():
|
||||
self.fields['set_as_primary'].initial = True
|
||||
291
netbox/dcim/migrations/0001_initial.py
Normal file
291
netbox/dcim/migrations/0001_initial.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConsolePort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('connection_status', models.NullBooleanField(choices=[[False, b'Planned'], [True, b'Connected']], default=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsolePortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsoleServerPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsoleServerPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Device',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', utilities.fields.NullableCharField(blank=True, max_length=50, null=True, unique=True)),
|
||||
('serial', models.CharField(blank=True, max_length=50, verbose_name=b'Serial number')),
|
||||
('position', models.PositiveSmallIntegerField(blank=True, help_text=b'Number of the lowest U position occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)')),
|
||||
('face', models.PositiveSmallIntegerField(blank=True, choices=[[0, b'Front'], [1, b'Rear']], null=True, verbose_name=b'Rack face')),
|
||||
('status', models.BooleanField(choices=[[True, b'Active'], [False, b'Offline']], default=True, verbose_name=b'Status')),
|
||||
('ro_snmp', models.CharField(blank=True, max_length=50, verbose_name=b'SNMP (RO)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceRole',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceType',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('model', models.CharField(max_length=50)),
|
||||
('slug', models.SlugField()),
|
||||
('u_height', models.PositiveSmallIntegerField(default=1, verbose_name=b'Height (U)')),
|
||||
('is_full_depth', models.BooleanField(default=True, help_text=b'Device consumes both front and rear rack faces', verbose_name=b'Is full depth')),
|
||||
('is_console_server', models.BooleanField(default=False, help_text=b'Include this type of device in lists of console servers', verbose_name=b'Is a console server')),
|
||||
('is_pdu', models.BooleanField(default=False, help_text=b'Include this type of device in lists of PDUs', verbose_name=b'Is a PDU')),
|
||||
('is_network_device', models.BooleanField(default=True, help_text=b'This is a network device (e.g. switch, router, etc.)', verbose_name=b'Is a network device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['manufacturer', 'model'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Interface',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('form_factor', models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (Copper)'], [1000, b'1GE (Copper)'], [1100, b'1GE (SFP)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200)),
|
||||
('mgmt_only', models.BooleanField(default=False, verbose_name=b'OOB Management')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterfaceConnection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('connection_status', models.BooleanField(choices=[[False, b'Planned'], [True, b'Connected']], default=True, verbose_name=b'Status')),
|
||||
('interface_a', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='connected_as_a', to='dcim.Interface')),
|
||||
('interface_b', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='connected_as_b', to='dcim.Interface')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterfaceTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('form_factor', models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (Copper)'], [1000, b'1GE (Copper)'], [1100, b'1GE (SFP)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200)),
|
||||
('mgmt_only', models.BooleanField(default=False, verbose_name=b'Management only')),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interface_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Manufacturer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Module',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name=b'Name')),
|
||||
('part_id', models.CharField(blank=True, max_length=50, verbose_name=b'Part ID')),
|
||||
('serial', models.CharField(blank=True, max_length=50, verbose_name=b'Serial number')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Platform',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('rpc_client', models.CharField(blank=True, choices=[[b'juniper-junos', b'Juniper Junos (NETCONF)'], [b'cisco-ios', b'Cisco IOS (SSH)'], [b'opengear', b'Opengear (SSH)']], max_length=30, verbose_name=b'RPC client')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerOutlet',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_outlets', to='dcim.Device')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerOutletTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_outlet_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('connection_status', models.NullBooleanField(choices=[[False, b'Planned'], [True, b'Connected']], default=True)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_ports', to='dcim.Device')),
|
||||
('power_outlet', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_port', to='dcim.PowerOutlet')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_port_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rack',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('facility_id', utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name=b'Facility ID')),
|
||||
('u_height', models.PositiveSmallIntegerField(default=42, verbose_name=b'Height (U)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RackGroup',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('slug', models.SlugField()),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Site',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('facility', models.CharField(blank=True, max_length=50)),
|
||||
('asn', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'ASN')),
|
||||
('physical_address', models.CharField(blank=True, max_length=200)),
|
||||
('shipping_address', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rack_groups', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='racks', to='dcim.RackGroup'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='device_types', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='device_role',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.DeviceRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='platform',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='dcim.Platform'),
|
||||
),
|
||||
]
|
||||
114
netbox/dcim/migrations/0002_auto_20160227_0235.py
Normal file
114
netbox/dcim/migrations/0002_auto_20160227_0235.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
('ipam', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='primary_ip',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_for', to='ipam.IPAddress', verbose_name=b'Primary IP'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_ports', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name=b'Console server port'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_ports', to='dcim.Device'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rackgroup',
|
||||
unique_together=set([('site', 'name'), ('site', 'slug')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together=set([('site', 'facility_id'), ('site', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlettemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlet',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='module',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interfacetemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interface',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicetype',
|
||||
unique_together=set([('manufacturer', 'slug'), ('manufacturer', 'model')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together=set([('rack', 'position', 'face')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
]
|
||||
0
netbox/dcim/migrations/__init__.py
Normal file
0
netbox/dcim/migrations/__init__.py
Normal file
686
netbox/dcim/models.py
Normal file
686
netbox/dcim/models.py
Normal file
@@ -0,0 +1,686 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q, ObjectDoesNotExist
|
||||
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from secrets.models import Secret
|
||||
from utilities.fields import NullableCharField
|
||||
|
||||
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
RACK_FACE_CHOICES = [
|
||||
[RACK_FACE_FRONT, 'Front'],
|
||||
[RACK_FACE_REAR, 'Rear'],
|
||||
]
|
||||
|
||||
COLOR_TEAL = 'teal'
|
||||
COLOR_GREEN = 'green'
|
||||
COLOR_BLUE = 'blue'
|
||||
COLOR_PURPLE = 'purple'
|
||||
COLOR_YELLOW = 'yellow'
|
||||
COLOR_ORANGE = 'orange'
|
||||
COLOR_RED = 'red'
|
||||
COLOR_GRAY1 = 'light_gray'
|
||||
COLOR_GRAY2 = 'medium_gray'
|
||||
COLOR_GRAY3 = 'dark_gray'
|
||||
DEVICE_ROLE_COLOR_CHOICES = [
|
||||
[COLOR_TEAL, 'Teal'],
|
||||
[COLOR_GREEN, 'Green'],
|
||||
[COLOR_BLUE, 'Blue'],
|
||||
[COLOR_PURPLE, 'Purple'],
|
||||
[COLOR_YELLOW, 'Yellow'],
|
||||
[COLOR_ORANGE, 'Orange'],
|
||||
[COLOR_RED, 'Red'],
|
||||
[COLOR_GRAY1, 'Light Gray'],
|
||||
[COLOR_GRAY2, 'Medium Gray'],
|
||||
[COLOR_GRAY3, 'Dark Gray'],
|
||||
]
|
||||
|
||||
IFACE_FF_VIRTUAL = 0
|
||||
IFACE_FF_100M_COPPER = 800
|
||||
IFACE_FF_1GE_COPPER = 1000
|
||||
IFACE_FF_SFP = 1100
|
||||
IFACE_FF_SFP_PLUS = 1200
|
||||
IFACE_FF_XFP = 1300
|
||||
IFACE_FF_QSFP_PLUS = 1400
|
||||
IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||
[IFACE_FF_100M_COPPER, '10/100M (Copper)'],
|
||||
[IFACE_FF_1GE_COPPER, '1GE (Copper)'],
|
||||
[IFACE_FF_SFP, '1GE (SFP)'],
|
||||
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
||||
[IFACE_FF_XFP, '10GE (XFP)'],
|
||||
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
|
||||
]
|
||||
|
||||
STATUS_ACTIVE = True
|
||||
STATUS_OFFLINE = False
|
||||
STATUS_CHOICES = [
|
||||
[STATUS_ACTIVE, 'Active'],
|
||||
[STATUS_OFFLINE, 'Offline'],
|
||||
]
|
||||
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
[CONNECTION_STATUS_PLANNED, 'Planned'],
|
||||
[CONNECTION_STATUS_CONNECTED, 'Connected'],
|
||||
]
|
||||
|
||||
# For mapping platform -> NC client
|
||||
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
|
||||
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
|
||||
RPC_CLIENT_OPENGEAR = 'opengear'
|
||||
RPC_CLIENT_CHOICES = [
|
||||
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
|
||||
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
|
||||
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
|
||||
]
|
||||
|
||||
|
||||
class Site(models.Model):
|
||||
"""
|
||||
A physical site
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
facility = models.CharField(max_length=50, blank=True)
|
||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
||||
physical_address = models.CharField(max_length=200, blank=True)
|
||||
shipping_address = models.CharField(max_length=200, blank=True)
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:site', args=[self.slug])
|
||||
|
||||
@property
|
||||
def count_prefixes(self):
|
||||
return self.prefixes.count()
|
||||
|
||||
@property
|
||||
def count_vlans(self):
|
||||
return self.vlans.count()
|
||||
|
||||
@property
|
||||
def count_racks(self):
|
||||
return Rack.objects.filter(site=self).count()
|
||||
|
||||
@property
|
||||
def count_devices(self):
|
||||
return Device.objects.filter(rack__site=self).count()
|
||||
|
||||
@property
|
||||
def count_circuits(self):
|
||||
return self.circuits.count()
|
||||
|
||||
|
||||
class RackGroup(models.Model):
|
||||
"""
|
||||
An arbitrary grouping of Racks; e.g. a building or room.
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
site = models.ForeignKey('Site', related_name='rack_groups')
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Rack(models.Model):
|
||||
"""
|
||||
An equipment rack within a site (e.g. a 48U rack)
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
|
||||
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
|
||||
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'facility_id'],
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
if self.facility_id:
|
||||
return "{} ({})".format(self.name, self.facility_id)
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
|
||||
def get_rack_units(self, face=RACK_FACE_FRONT, remove_redundant=False):
|
||||
"""
|
||||
Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
|
||||
Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
|
||||
|
||||
:param face: Rack face (front or rear)
|
||||
:param remove_redundant: If True, rack units occupied by a device already listed will be omitted
|
||||
"""
|
||||
|
||||
elevation = OrderedDict()
|
||||
for u in reversed(range(1, self.u_height + 1)):
|
||||
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
|
||||
.filter(rack=self, position__gt=0).filter(Q(face=face) | Q(device_type__is_full_depth=True)):
|
||||
if remove_redundant:
|
||||
elevation[device.position]['device'] = device
|
||||
for u in range(device.position + 1, device.position + device.device_type.u_height):
|
||||
elevation.pop(u, None)
|
||||
else:
|
||||
for u in range(device.position, device.position + device.device_type.u_height):
|
||||
elevation[u]['device'] = device
|
||||
|
||||
return [u for u in elevation.values()]
|
||||
|
||||
def get_front_elevation(self):
|
||||
return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True)
|
||||
|
||||
def get_rear_elevation(self):
|
||||
return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True)
|
||||
|
||||
def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
|
||||
"""
|
||||
Return a list of units within the rack available to accommodate a device of a given U height (default 1).
|
||||
Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
|
||||
position to another within a rack).
|
||||
|
||||
:param u_height: Minimum number of contiguous free units required
|
||||
:param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
|
||||
:param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
|
||||
"""
|
||||
|
||||
# Gather all devices which consume U space within the rack
|
||||
devices = self.devices.select_related().filter(position__gte=1).exclude(pk__in=exclude)
|
||||
|
||||
# Initialize the rack unit skeleton
|
||||
units = range(1, self.u_height + 1)
|
||||
|
||||
# Remove units consumed by installed devices
|
||||
for d in devices:
|
||||
if rack_face is None or d.face == rack_face or d.device_type.is_full_depth:
|
||||
for u in range(d.position, d.position + d.device_type.u_height):
|
||||
try:
|
||||
units.remove(u)
|
||||
except ValueError:
|
||||
# Found overlapping devices in the rack!
|
||||
pass
|
||||
|
||||
# Remove units without enough space above them to accommodate a device of the specified height
|
||||
available_units = []
|
||||
for u in units:
|
||||
if set(range(u, u + u_height)).issubset(units):
|
||||
available_units.append(u)
|
||||
|
||||
return list(reversed(available_units))
|
||||
|
||||
def get_0u_devices(self):
|
||||
return self.devices.filter(position=0)
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
#
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
"""
|
||||
A hardware manufacturer
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class DeviceType(models.Model):
|
||||
"""
|
||||
A unique hardware type; manufacturer and model number (e.g. Juniper EX4300-48T)
|
||||
"""
|
||||
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
|
||||
model = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
||||
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
||||
help_text="Device consumes both front and rear rack faces")
|
||||
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
||||
help_text="Include this type of device in lists of console servers")
|
||||
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
||||
help_text="Include this type of device in lists of PDUs")
|
||||
is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
|
||||
help_text="This is a network device (e.g. switch, router, etc.)")
|
||||
|
||||
class Meta:
|
||||
ordering = ['manufacturer', 'model']
|
||||
unique_together = [
|
||||
['manufacturer', 'model'],
|
||||
['manufacturer', 'slug'],
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return "{0} {1}".format(self.manufacturer, self.model)
|
||||
|
||||
|
||||
class ConsolePortTemplate(models.Model):
|
||||
"""
|
||||
A template for a ConsolePort to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ConsoleServerPortTemplate(models.Model):
|
||||
"""
|
||||
A template for a ConsoleServerPort to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerPortTemplate(models.Model):
|
||||
"""
|
||||
A template for a PowerPort to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerOutletTemplate(models.Model):
|
||||
"""
|
||||
A template for a PowerOutlet to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InterfaceTemplate(models.Model):
|
||||
"""
|
||||
A template for a physical data interface on a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRole(models.Model):
|
||||
"""
|
||||
The functional role of a device (e.g. router, switch, console server, etc.)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
color = models.CharField(max_length=30, choices=DEVICE_ROLE_COLOR_CHOICES)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
"""
|
||||
A class of software running on a hardware device (e.g. Juniper Junos or Cisco IOS)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client')
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Device(models.Model):
|
||||
"""
|
||||
A physical piece of equipment mounted within a rack
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
||||
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
|
||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
|
||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)', help_text='Number of the lowest U position occupied by the device')
|
||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
||||
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||
primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IP')
|
||||
ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)')
|
||||
comments = models.TextField(blank=True)
|
||||
secrets = GenericRelation(Secret)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.display_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.position:
|
||||
return "{} ({} U{})".format(self.device_type, self.rack, self.position)
|
||||
else:
|
||||
return "{} ({})".format(self.device_type, self.rack)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate position/face combination
|
||||
if self.position and self.face is None:
|
||||
raise ValidationError("Must specify rack face with rack position.")
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError("U{} is already occupied or does not have sufficient space to accommodate a(n) "
|
||||
"{} ({}U).".format(self.position, self.device_type, self.device_type.u_height))
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
is_new = not bool(self.pk)
|
||||
|
||||
super(Device, self).save(*args, **kwargs)
|
||||
|
||||
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
||||
if is_new:
|
||||
ConsolePort.objects.bulk_create(
|
||||
[ConsolePort(device=self, name=template.name) for template in self.device_type.console_port_templates.all()]
|
||||
)
|
||||
ConsoleServerPort.objects.bulk_create(
|
||||
[ConsoleServerPort(device=self, name=template.name) for template in self.device_type.cs_port_templates.all()]
|
||||
)
|
||||
PowerPort.objects.bulk_create(
|
||||
[PowerPort(device=self, name=template.name) for template in self.device_type.power_port_templates.all()]
|
||||
)
|
||||
PowerOutlet.objects.bulk_create(
|
||||
[PowerOutlet(device=self, name=template.name) for template in self.device_type.power_outlet_templates.all()]
|
||||
)
|
||||
Interface.objects.bulk_create(
|
||||
[Interface(device=self, name=template.name, form_factor=template.form_factor, mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
|
||||
)
|
||||
|
||||
def get_rpc_client(self):
|
||||
"""
|
||||
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
|
||||
"""
|
||||
if not self.platform:
|
||||
return None
|
||||
return RPC_CLIENTS.get(self.platform.rpc_client)
|
||||
|
||||
|
||||
class ConsolePort(models.Model):
|
||||
"""
|
||||
A physical console port on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL, verbose_name='Console server port', blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ConsoleServerPortManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Include the trailing numeric portion of each port name to allow for proper ordering.
|
||||
For example:
|
||||
Port 1, Port 2, Port 3 ... Port 9, Port 10, Port 11 ...
|
||||
Instead of:
|
||||
Port 1, Port 10, Port 11 ... Port 19, Port 2, Port 20 ...
|
||||
"""
|
||||
return super(ConsoleServerPortManager, self).get_queryset().extra(select={
|
||||
'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)",
|
||||
}).order_by('device', 'name_as_integer')
|
||||
|
||||
|
||||
class ConsoleServerPort(models.Model):
|
||||
"""
|
||||
A physical port on a console server
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
objects = ConsoleServerPortManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerPort(models.Model):
|
||||
"""
|
||||
A physical power supply (intake) port on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL, blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerOutletManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
return super(PowerOutletManager, self).get_queryset().extra(select={
|
||||
'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))",
|
||||
}).order_by('device', 'name_padded')
|
||||
|
||||
|
||||
class PowerOutlet(models.Model):
|
||||
"""
|
||||
A physical power outlet (output) port on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
objects = PowerOutletManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InterfaceManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Cast up to three interface slot/position IDs as independent integers and order appropriately. This ensures that
|
||||
interfaces are ordered numerically without regard to type. For example:
|
||||
xe-0/0/0, xe-0/0/1, xe-0/0/2 ... et-0/0/47, et-0/0/48, et-0/0/49 ...
|
||||
instead of:
|
||||
et-0/0/48, et-0/0/49, et-0/0/50 ... et-0/0/53, xe-0/0/0, xe-0/0/1 ...
|
||||
"""
|
||||
return super(InterfaceManager, self).get_queryset().extra(select={
|
||||
'_id1': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)\/([0-9]+)$') AS integer)",
|
||||
'_id2': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)$') AS integer)",
|
||||
'_id3': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)$') AS integer)",
|
||||
}).order_by('device', '_id1', '_id2', '_id3')
|
||||
|
||||
def virtual(self):
|
||||
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
|
||||
|
||||
def physical(self):
|
||||
return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL)
|
||||
|
||||
|
||||
class Interface(models.Model):
|
||||
"""
|
||||
A physical data interface on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
||||
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management')
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
objects = InterfaceManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_physical(self):
|
||||
return self.form_factor != IFACE_FF_VIRTUAL
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
try:
|
||||
return bool(self.circuit)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return bool(self.connection)
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
try:
|
||||
return self.connected_as_a
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
try:
|
||||
return self.connected_as_b
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_connected_interface(self):
|
||||
try:
|
||||
connection = InterfaceConnection.objects.select_related().get(Q(interface_a=self) | Q(interface_b=self))
|
||||
if connection.interface_a == self:
|
||||
return connection.interface_b
|
||||
else:
|
||||
return connection.interface_a
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
return None
|
||||
except InterfaceConnection.MultipleObjectsReturned as e:
|
||||
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
|
||||
|
||||
|
||||
class InterfaceConnection(models.Model):
|
||||
"""
|
||||
A symmetrical, one-to-one connection between two device interfaces
|
||||
"""
|
||||
interface_a = models.OneToOneField('Interface', related_name='connected_as_a', on_delete=models.CASCADE)
|
||||
interface_b = models.OneToOneField('Interface', related_name='connected_as_b', on_delete=models.CASCADE)
|
||||
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED, verbose_name='Status')
|
||||
|
||||
|
||||
class Module(models.Model):
|
||||
"""
|
||||
A hardware module belonging to a device. Used for inventory purposes only.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=50, verbose_name='Name')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
165
netbox/dcim/tables.py
Normal file
165
netbox/dcim/tables.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from .models import Site, Rack, Device, ConsolePort, PowerPort
|
||||
|
||||
|
||||
PREFIXES_PER_VLAN = """
|
||||
{% for p in record.prefix_set.all %}
|
||||
<a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a>
|
||||
{% if not forloop.last %}<br />{% endif %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.status.get_bootstrap_class_display|lower }}">
|
||||
{{ record.status.name }}
|
||||
</span>
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
<a href="{% url 'dcim:device' pk=record.pk %}">{{ record.name|default:'<span class="label label-info">Unnamed device</span>' }}</a>
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteTable(tables.Table):
|
||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||
facility = tables.Column(verbose_name='Facility')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count', 'circuit_count')
|
||||
empty_text = "No sites have been defined."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackTable(tables.Table):
|
||||
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
group = tables.Column(verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
u_height = tables.Column(verbose_name='Height (U)')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), orderable=False, verbose_name='Devices')
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ('name', 'site', 'group', 'facility_id', 'u_height')
|
||||
empty_text = "No racks were found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class RackBulkEditTable(RackTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceTable(tables.Table):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
device_role = tables.Column(verbose_name='Role')
|
||||
device_type = tables.Column(verbose_name='Type')
|
||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address', template_code="{{ record.primary_ip.address.ip }}")
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ('name', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
empty_text = "No devices were found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class DeviceBulkEditTable(DeviceTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(DeviceTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
|
||||
|
||||
class DeviceImportTable(tables.Table):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
position = tables.Column(verbose_name='Position')
|
||||
device_role = tables.Column(verbose_name='Role')
|
||||
device_type = tables.Column(verbose_name='Type')
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTable(tables.Table):
|
||||
console_server = tables.LinkColumn('dcim:device', accessor=Accessor('cs_port.device'), args=[Accessor('cs_port.device.pk')], verbose_name='Console server')
|
||||
cs_port = tables.Column(verbose_name='Port')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ('console_server', 'cs_port', 'device', 'name')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class PowerConnectionTable(tables.Table):
|
||||
pdu = tables.LinkColumn('dcim:device', accessor=Accessor('power_outlet.device'), args=[Accessor('power_outlet.device.pk')], verbose_name='PDU')
|
||||
power_outlet = tables.Column(verbose_name='Outlet')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ('pdu', 'power_outlet', 'device', 'name')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class InterfaceConnectionTable(tables.Table):
|
||||
device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'), args=[Accessor('interface_a.device.pk')], verbose_name='Device A')
|
||||
interface_a = tables.Column(verbose_name='Interface A')
|
||||
device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'), args=[Accessor('interface_b.device.pk')], verbose_name='Device B')
|
||||
interface_b = tables.Column(verbose_name='Interface B')
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
0
netbox/dcim/tests/__init__.py
Normal file
0
netbox/dcim/tests/__init__.py
Normal file
71
netbox/dcim/tests/test_forms.py
Normal file
71
netbox/dcim/tests/test_forms.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from django.test import TestCase
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
def get_id(model, slug):
|
||||
return model.objects.get(slug=slug).id
|
||||
|
||||
|
||||
class DeviceTestCase(TestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
def test_racked_device(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': RACK_FACE_FRONT,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'position': 41,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
||||
})
|
||||
self.assertTrue(test.is_valid(), test.fields['position'].choices)
|
||||
self.assertTrue(test.save())
|
||||
|
||||
def test_racked_device_occupied(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': RACK_FACE_FRONT,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'position': 1,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
||||
})
|
||||
self.assertFalse(test.is_valid())
|
||||
|
||||
def test_non_racked_device(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'pdu'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': None,
|
||||
'platform': None,
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'position': None,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
||||
})
|
||||
self.assertTrue(test.is_valid())
|
||||
self.assertTrue(test.save())
|
||||
|
||||
def test_non_racked_device_with_face(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'pdu'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': RACK_FACE_REAR,
|
||||
'platform': None,
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'position': None,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
||||
})
|
||||
self.assertTrue(test.is_valid())
|
||||
self.assertTrue(test.save())
|
||||
96
netbox/dcim/tests/test_models.py
Normal file
96
netbox/dcim/tests/test_models.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from django.test import TestCase
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
site = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=site,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
name='Acme',
|
||||
slug='acme'
|
||||
)
|
||||
|
||||
self.device_type = {
|
||||
'ff2048': DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='FrameForwarder 2048',
|
||||
slug='ff2048'
|
||||
),
|
||||
'cc5000': DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='CurrentCatapult 5000',
|
||||
slug='cc5000',
|
||||
u_height=0
|
||||
),
|
||||
}
|
||||
self.role = {
|
||||
'Server': DeviceRole.objects.create(
|
||||
name='Server',
|
||||
slug='server',
|
||||
),
|
||||
'Switch': DeviceRole.objects.create(
|
||||
name='Switch',
|
||||
slug='switch',
|
||||
),
|
||||
'Console Server': DeviceRole.objects.create(
|
||||
name='Console Server',
|
||||
slug='console-server',
|
||||
),
|
||||
'PDU': DeviceRole.objects.create(
|
||||
name='PDU',
|
||||
slug='pdu',
|
||||
),
|
||||
|
||||
}
|
||||
|
||||
def test_mount_single_device(self):
|
||||
|
||||
rack1 = Rack.objects.get(name='TestRack1')
|
||||
device1 = Device(
|
||||
name='TestSwitch1',
|
||||
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||
device_role=DeviceRole.objects.get(slug='switch'),
|
||||
rack=rack1,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
device1.save()
|
||||
|
||||
# Validate rack height
|
||||
self.assertEqual(list(rack1.units), list(reversed(range(1, 43))))
|
||||
|
||||
# Validate inventory (front face)
|
||||
rack1_inventory_front = rack1.get_front_elevation()
|
||||
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
||||
del(rack1_inventory_front[-10])
|
||||
for u in rack1_inventory_front:
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
# Validate inventory (rear face)
|
||||
rack1_inventory_rear = rack1.get_rear_elevation()
|
||||
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
||||
del(rack1_inventory_rear[-10])
|
||||
for u in rack1_inventory_rear:
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
def test_mount_zero_ru(self):
|
||||
pdu = Device.objects.create(
|
||||
name='TestPDU',
|
||||
device_role=self.role.get('PDU'),
|
||||
device_type=self.device_type.get('cc5000'),
|
||||
rack=self.rack,
|
||||
position=None,
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
||||
86
netbox/dcim/urls.py
Normal file
86
netbox/dcim/urls.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from secrets.views import secret_add
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', views.site_list, name='site_list'),
|
||||
url(r'^sites/add/$', views.site_add, name='site_add'),
|
||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.site_edit, name='site_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.site_delete, name='site_delete'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.rack_list, name='rack_list'),
|
||||
url(r'^racks/add/$', views.rack_add, name='rack_add'),
|
||||
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.rack_edit, name='rack_edit'),
|
||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.rack_delete, name='rack_delete'),
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', views.device_list, name='device_list'),
|
||||
url(r'^devices/add/$', views.device_add, name='device_add'),
|
||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.device_edit, name='device_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.device_delete, name='device_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^devices/(?P<parent_pk>\d+)/add-secret/$', secret_add, {'parent_model': 'dcim.Device'},
|
||||
name='device_addsecret'),
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.consoleport_delete, name='consoleport_delete'),
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.consoleserverport_delete, name='consoleserverport_delete'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.powerport_delete, name='powerport_delete'),
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_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'),
|
||||
url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
|
||||
url(r'^power-connections/import/$', views.PowerConnectionsBulkImportView.as_view(), name='power_connections_import'),
|
||||
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'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_bulk_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.interface_delete, name='interface_delete'),
|
||||
|
||||
]
|
||||
1444
netbox/dcim/views.py
Normal file
1444
netbox/dcim/views.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user