Initial push to public repo

This commit is contained in:
Jeremy Stretch
2016-03-01 11:23:03 -05:00
commit 27b289ee3b
281 changed files with 26061 additions and 0 deletions

1
netbox/dcim/__init__.py Normal file
View File

@@ -0,0 +1 @@
default_app_config = 'dcim.apps.IPAMConfig'

161
netbox/dcim/admin.py Normal file
View 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')

View File

View 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."

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

File diff suppressed because it is too large Load Diff

953
netbox/dcim/forms.py Normal file
View 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

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

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

View File

686
netbox/dcim/models.py Normal file
View 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
View 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',
}

View File

View 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())

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

File diff suppressed because it is too large Load Diff