mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #3061 from digitalocean/54-power-modeling
#54: Power modeling
This commit is contained in:
commit
ea6815b9bb
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0014_circuittermination_description'),
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -43,7 +43,7 @@ urlpatterns = [
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
|
||||
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/trace/$', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
||||
|
||||
]
|
||||
|
@ -3,8 +3,8 @@ from rest_framework import serializers
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
|
||||
Interface, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
|
||||
Region, Site, VirtualChassis,
|
||||
Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, Rack, RackGroup, RackRole,
|
||||
RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from utilities.api import ChoiceField, WritableNestedSerializer
|
||||
|
||||
@ -21,7 +21,9 @@ __all__ = [
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedManufacturerSerializer',
|
||||
'NestedPlatformSerializer',
|
||||
'NestedPowerFeedSerializer',
|
||||
'NestedPowerOutletSerializer',
|
||||
'NestedPowerPanelSerializer',
|
||||
'NestedPowerPortSerializer',
|
||||
'NestedRackGroupSerializer',
|
||||
'NestedRackRoleSerializer',
|
||||
@ -247,3 +249,23 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'master']
|
||||
|
||||
|
||||
#
|
||||
# Power panels/feeds
|
||||
#
|
||||
|
||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = ['id', 'url', 'name']
|
||||
|
@ -8,8 +8,9 @@ from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
@ -209,15 +210,23 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'device_type', 'name']
|
||||
fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
power_port = PowerPortTemplateSerializer(
|
||||
required=False
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'device_type', 'name']
|
||||
fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -371,14 +380,26 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
cable = NestedCableSerializer(
|
||||
read_only=True
|
||||
)
|
||||
tags = TagListSerializerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'cable', 'tags',
|
||||
'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@ -390,7 +411,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'cable', 'tags',
|
||||
]
|
||||
|
||||
@ -592,3 +613,56 @@ class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'master', 'domain', 'tags']
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
|
||||
class PowerPanelSerializer(ValidatedModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
rack_group = NestedRackGroupSerializer(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'site', 'rack_group', 'name']
|
||||
|
||||
|
||||
class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=POWERFEED_TYPE_CHOICES,
|
||||
default=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=POWERFEED_STATUS_CHOICES,
|
||||
default=POWERFEED_STATUS_ACTIVE
|
||||
)
|
||||
supply = ChoiceField(
|
||||
choices=POWERFEED_SUPPLY_CHOICES,
|
||||
default=POWERFEED_SUPPLY_AC
|
||||
)
|
||||
phase = ChoiceField(
|
||||
choices=POWERFEED_PHASE_CHOICES,
|
||||
default=POWERFEED_PHASE_SINGLE
|
||||
)
|
||||
tags = TagListSerializerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'power_factor', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
@ -68,6 +68,10 @@ router.register(r'cables', views.CableViewSet)
|
||||
# Virtual chassis
|
||||
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||
|
||||
# Power
|
||||
router.register(r'power-panels', views.PowerPanelViewSet)
|
||||
router.register(r'power-feeds', views.PowerFeedViewSet)
|
||||
|
||||
# Miscellaneous
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
|
||||
|
||||
|
@ -16,8 +16,9 @@ from dcim import filters
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
@ -43,6 +44,8 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
(FrontPortTemplate, ['type']),
|
||||
(Interface, ['form_factor', 'mode']),
|
||||
(InterfaceTemplate, ['form_factor']),
|
||||
(PowerOutlet, ['feed_leg']),
|
||||
(PowerOutletTemplate, ['feed_leg']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(Rack, ['outer_unit', 'status', 'type', 'width']),
|
||||
(RearPort, ['type']),
|
||||
@ -407,7 +410,7 @@ class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
|
||||
class PowerPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device', 'cable'
|
||||
'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
@ -497,7 +500,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = PowerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
_connected_poweroutlet__isnull=False
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filters.PowerConnectionFilter
|
||||
@ -536,6 +539,26 @@ class VirtualChassisViewSet(ModelViewSet):
|
||||
serializer_class = serializers.VirtualChassisSerializer
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelViewSet(ModelViewSet):
|
||||
queryset = PowerPanel.objects.select_related('site', 'rack_group')
|
||||
serializer_class = serializers.PowerPanelSerializer
|
||||
filterset_class = filters.PowerPanelFilter
|
||||
|
||||
|
||||
#
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
class PowerFeedViewSet(CustomFieldModelViewSet):
|
||||
queryset = PowerFeed.objects.select_related('power_panel', 'rack').prefetch_related('tags')
|
||||
serializer_class = serializers.PowerFeedSerializer
|
||||
filterset_class = filters.PowerFeedFilter
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
@ -422,7 +422,7 @@ CABLE_TERMINATION_TYPE_CHOICES = {
|
||||
COMPATIBLE_TERMINATION_TYPES = {
|
||||
'consoleport': ['consoleserverport', 'frontport', 'rearport'],
|
||||
'consoleserverport': ['consoleport', 'frontport', 'rearport'],
|
||||
'powerport': ['poweroutlet'],
|
||||
'powerport': ['poweroutlet', 'powerfeed'],
|
||||
'poweroutlet': ['powerport'],
|
||||
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
|
||||
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
@ -445,3 +445,41 @@ RACK_DIMENSION_UNIT_CHOICES = (
|
||||
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
|
||||
(LENGTH_UNIT_INCH, 'Inches'),
|
||||
)
|
||||
|
||||
# Power feeds
|
||||
POWERFEED_TYPE_PRIMARY = 1
|
||||
POWERFEED_TYPE_REDUNDANT = 2
|
||||
POWERFEED_TYPE_CHOICES = (
|
||||
(POWERFEED_TYPE_PRIMARY, 'Primary'),
|
||||
(POWERFEED_TYPE_REDUNDANT, 'Redundant'),
|
||||
)
|
||||
POWERFEED_SUPPLY_AC = 1
|
||||
POWERFEED_SUPPLY_DC = 2
|
||||
POWERFEED_SUPPLY_CHOICES = (
|
||||
(POWERFEED_SUPPLY_AC, 'AC'),
|
||||
(POWERFEED_SUPPLY_DC, 'DC'),
|
||||
)
|
||||
POWERFEED_PHASE_SINGLE = 1
|
||||
POWERFEED_PHASE_3PHASE = 3
|
||||
POWERFEED_PHASE_CHOICES = (
|
||||
(POWERFEED_PHASE_SINGLE, 'Single phase'),
|
||||
(POWERFEED_PHASE_3PHASE, 'Three-phase'),
|
||||
)
|
||||
POWERFEED_STATUS_OFFLINE = 0
|
||||
POWERFEED_STATUS_ACTIVE = 1
|
||||
POWERFEED_STATUS_PLANNED = 2
|
||||
POWERFEED_STATUS_FAILED = 4
|
||||
POWERFEED_STATUS_CHOICES = (
|
||||
(POWERFEED_STATUS_ACTIVE, 'Active'),
|
||||
(POWERFEED_STATUS_OFFLINE, 'Offline'),
|
||||
(POWERFEED_STATUS_PLANNED, 'Planned'),
|
||||
(POWERFEED_STATUS_FAILED, 'Failed'),
|
||||
)
|
||||
POWERFEED_LEG_A = 1
|
||||
POWERFEED_LEG_B = 2
|
||||
POWERFEED_LEG_C = 3
|
||||
POWERFEED_LEG_CHOICES = (
|
||||
(POWERFEED_LEG_A, 'A'),
|
||||
(POWERFEED_LEG_B, 'B'),
|
||||
(POWERFEED_LEG_C, 'C'),
|
||||
)
|
||||
|
@ -15,8 +15,9 @@ from .constants import *
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@ -37,7 +38,7 @@ class RegionFilter(NameSlugSearchFilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class SiteFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@ -122,7 +123,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
|
||||
fields = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class RackFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@ -1065,3 +1066,86 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
Q(device__name__icontains=value) |
|
||||
Q(_connected_interface__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class PowerPanelFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack_group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PowerFeedFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='power_panel__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='power_panel__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
power_panel_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
label='Power panel (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'power_factor']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
@ -2667,7 +2667,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM0",
|
||||
"connected_endpoint": 25,
|
||||
"_connected_poweroutlet": 25,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2677,7 +2677,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM1",
|
||||
"connected_endpoint": 49,
|
||||
"_connected_poweroutlet": 49,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2687,7 +2687,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM2",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2697,7 +2697,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM3",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2707,7 +2707,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM0",
|
||||
"connected_endpoint": 26,
|
||||
"_connected_poweroutlet": 26,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2717,7 +2717,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM1",
|
||||
"connected_endpoint": 50,
|
||||
"_connected_poweroutlet": 50,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2727,7 +2727,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM2",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2737,7 +2737,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM3",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2747,7 +2747,7 @@
|
||||
"fields": {
|
||||
"device": 4,
|
||||
"name": "PSU0",
|
||||
"connected_endpoint": 28,
|
||||
"_connected_poweroutlet": 28,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2757,7 +2757,7 @@
|
||||
"fields": {
|
||||
"device": 4,
|
||||
"name": "PSU1",
|
||||
"connected_endpoint": 52,
|
||||
"_connected_poweroutlet": 52,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2767,7 +2767,7 @@
|
||||
"fields": {
|
||||
"device": 5,
|
||||
"name": "PSU0",
|
||||
"connected_endpoint": 56,
|
||||
"_connected_poweroutlet": 56,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2777,7 +2777,7 @@
|
||||
"fields": {
|
||||
"device": 5,
|
||||
"name": "PSU1",
|
||||
"connected_endpoint": 32,
|
||||
"_connected_poweroutlet": 32,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2787,7 +2787,7 @@
|
||||
"fields": {
|
||||
"device": 3,
|
||||
"name": "PSU0",
|
||||
"connected_endpoint": 27,
|
||||
"_connected_poweroutlet": 27,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2797,7 +2797,7 @@
|
||||
"fields": {
|
||||
"device": 3,
|
||||
"name": "PSU1",
|
||||
"connected_endpoint": 51,
|
||||
"_connected_poweroutlet": 51,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2807,7 +2807,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM0",
|
||||
"connected_endpoint": 53,
|
||||
"_connected_poweroutlet": 53,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2817,7 +2817,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM1",
|
||||
"connected_endpoint": 29,
|
||||
"_connected_poweroutlet": 29,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2827,7 +2827,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM2",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2837,7 +2837,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM3",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2847,7 +2847,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM0",
|
||||
"connected_endpoint": 54,
|
||||
"_connected_poweroutlet": 54,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2857,7 +2857,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM1",
|
||||
"connected_endpoint": 30,
|
||||
"_connected_poweroutlet": 30,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2867,7 +2867,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM2",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2877,7 +2877,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM3",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2887,7 +2887,7 @@
|
||||
"fields": {
|
||||
"device": 6,
|
||||
"name": "PSU0",
|
||||
"connected_endpoint": 55,
|
||||
"_connected_poweroutlet": 55,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2897,7 +2897,7 @@
|
||||
"fields": {
|
||||
"device": 6,
|
||||
"name": "PSU1",
|
||||
"connected_endpoint": 31,
|
||||
"_connected_poweroutlet": 31,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@ -2907,7 +2907,7 @@
|
||||
"fields": {
|
||||
"device": 9,
|
||||
"name": "PSU",
|
||||
"connected_endpoint": null,
|
||||
"_connected_poweroutlet": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
|
@ -10,24 +10,24 @@ from mptt.forms import TreeNodeChoiceField
|
||||
from taggit.forms import TagField
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
from circuits.models import Circuit, Provider
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField,
|
||||
ComponentForm, ConfirmationForm, ContentTypeSelect, CSVChoiceField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, JSONField, SelectWithPK, SmallTextarea, SlugField,
|
||||
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .constants import *
|
||||
from .models import (
|
||||
Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||
Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis
|
||||
InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate,
|
||||
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
DEVICE_BY_PK_RE = r'{\d+\}'
|
||||
@ -963,7 +963,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
'device_type', 'name', 'maximum_draw', 'allocated_draw',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -977,16 +977,29 @@ class PowerPortTemplateCreateForm(ComponentForm):
|
||||
|
||||
|
||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
'device_type', 'name', 'power_port', 'feed_leg',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to current DeviceType
|
||||
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
|
||||
device_type=self.parent
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
@ -1947,7 +1960,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'device', 'name', 'description', 'tags',
|
||||
'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -1972,6 +1985,10 @@ class PowerPortCreateForm(ComponentForm):
|
||||
#
|
||||
|
||||
class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
@ -1979,12 +1996,20 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'device', 'name', 'description', 'tags',
|
||||
'device', 'name', 'power_port', 'feed_leg', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to the local device
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(
|
||||
device=self.instance.device
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
@ -2004,6 +2029,10 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
required=False,
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@ -2520,7 +2549,10 @@ class RearPortBulkDisconnectForm(ConfirmationForm):
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
"""
|
||||
Base form for connecting a Cable to a Device component
|
||||
"""
|
||||
termination_b_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
@ -2566,39 +2598,196 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_type = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
label='Type',
|
||||
widget=ContentTypeSelect()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
|
||||
'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
|
||||
|
||||
class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/{{termination_b_type}}s/',
|
||||
api_url='/api/dcim/console-ports/',
|
||||
disabled_indicator='cable',
|
||||
conditional_query_params={
|
||||
'termination_b_type__interface': 'type=physical',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/console-server-ports/',
|
||||
disabled_indicator='cable',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-ports/',
|
||||
disabled_indicator='cable',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-outlets/',
|
||||
disabled_indicator='cable',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/interfaces/',
|
||||
disabled_indicator='cable',
|
||||
additional_query_params={
|
||||
'type': 'physical',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/front-ports/',
|
||||
disabled_indicator='cable',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/rear-ports/',
|
||||
disabled_indicator='cable',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
termination_b_provider = forms.ModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
widget=APISelect(
|
||||
api_url='/api/circuits/providers/',
|
||||
filter_for={
|
||||
'termination_b_circuit': 'provider_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/sites/',
|
||||
filter_for={
|
||||
'termination_b_circuit': 'site_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_circuit = ChainedModelChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
chains=(
|
||||
('provider', 'termination_b_provider'),
|
||||
),
|
||||
label='Circuit',
|
||||
widget=APISelect(
|
||||
api_url='/api/circuits/circuits/',
|
||||
display_field='cid',
|
||||
filter_for={
|
||||
'termination_b_id': 'circuit_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Side',
|
||||
widget=APISelect(
|
||||
api_url='/api/circuits/circuit-terminations/',
|
||||
disabled_indicator='cable',
|
||||
display_field='term_side'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_type',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
|
||||
'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
|
||||
'status', 'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Define available types for endpoint B based on the type of endpoint A
|
||||
termination_a_type = self.instance.termination_a._meta.model_name
|
||||
self.fields['termination_b_type'].queryset = ContentType.objects.filter(
|
||||
model__in=COMPATIBLE_TERMINATION_TYPES.get(termination_a_type)
|
||||
).exclude(
|
||||
model='circuittermination'
|
||||
class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
termination_b_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/sites/',
|
||||
display_field='cid',
|
||||
filter_for={
|
||||
'termination_b_rackgroup': 'site_id',
|
||||
'termination_b_powerpanel': 'site_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_rackgroup = ChainedModelChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack Group',
|
||||
chains=(
|
||||
('site', 'termination_b_site'),
|
||||
),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/rack-groups/',
|
||||
display_field='cid',
|
||||
filter_for={
|
||||
'termination_b_powerpanel': 'rackgroup_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_powerpanel = ChainedModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
chains=(
|
||||
('site', 'termination_b_site'),
|
||||
('rack_group', 'termination_b_rackgroup'),
|
||||
),
|
||||
label='Power Panel',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-panels/',
|
||||
filter_for={
|
||||
'termination_b_id': 'power_panel_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
termination_b_id = forms.IntegerField(
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-feeds/',
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_rackgroup', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
|
||||
'color', 'length', 'length_unit',
|
||||
]
|
||||
|
||||
|
||||
class CableForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -3155,3 +3344,346 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelForm(BootstrapMixin, forms.ModelForm):
|
||||
rack_group = ChainedModelChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/rack-groups/',
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = [
|
||||
'site', 'rack_group', 'name',
|
||||
]
|
||||
widgets = {
|
||||
'site': APISelect(
|
||||
api_url="/api/dcim/sites/",
|
||||
filter_for={
|
||||
'rack_group': 'site_id',
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class PowerPanelCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
)
|
||||
rack_group_name = forms.CharField(
|
||||
required=False,
|
||||
help_text="Rack group name (optional)"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = PowerPanel.csv_headers
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
rack_group_name = self.cleaned_data.get('rack_group_name')
|
||||
|
||||
# Validate rack group
|
||||
if rack_group_name:
|
||||
try:
|
||||
self.instance.rack_group = RackGroup.objects.get(site=site, name=rack_group_name)
|
||||
except RackGroup.DoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
"Rack group {} not found in site {}".format(rack_group_name, site)
|
||||
)
|
||||
|
||||
|
||||
class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = PowerPanel
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'rack_id': 'site',
|
||||
}
|
||||
)
|
||||
)
|
||||
rack_group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/rack-groups/",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
class PowerFeedForm(BootstrapMixin, CustomFieldForm):
|
||||
site = ChainedModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/sites/',
|
||||
filter_for={
|
||||
'power_panel': 'site_id',
|
||||
'rack': 'site_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'power_factor', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'power_panel': APISelect(
|
||||
api_url="/api/dcim/power-panels/"
|
||||
),
|
||||
'rack': APISelect(
|
||||
api_url="/api/dcim/racks/"
|
||||
),
|
||||
'status': StaticSelect2(),
|
||||
'type': StaticSelect2(),
|
||||
'supply': StaticSelect2(),
|
||||
'phase': StaticSelect2(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize site field
|
||||
if self.instance and self.instance.power_panel:
|
||||
self.initial['site'] = self.instance.power_panel.site
|
||||
|
||||
|
||||
class PowerFeedCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
)
|
||||
panel_name = forms.ModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of upstream power panel',
|
||||
error_messages={
|
||||
'invalid_choice': 'Power panel not found.',
|
||||
}
|
||||
)
|
||||
rack_group = forms.CharField(
|
||||
required=False,
|
||||
help_text="Rack group name (optional)"
|
||||
)
|
||||
rack_name = forms.CharField(
|
||||
required=False,
|
||||
help_text="Rack name (optional)"
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=POWERFEED_STATUS_CHOICES,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=POWERFEED_TYPE_CHOICES,
|
||||
required=False,
|
||||
help_text='Primary or redundant'
|
||||
)
|
||||
supply = CSVChoiceField(
|
||||
choices=POWERFEED_SUPPLY_CHOICES,
|
||||
required=False,
|
||||
help_text='AC/DC'
|
||||
)
|
||||
phase = CSVChoiceField(
|
||||
choices=POWERFEED_PHASE_CHOICES,
|
||||
required=False,
|
||||
help_text='Single or three-phase'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = PowerFeed.csv_headers
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
panel_name = self.cleaned_data.get('panel_name')
|
||||
rack_group = self.cleaned_data.get('rack_group')
|
||||
rack_name = self.cleaned_data.get('rack_name')
|
||||
|
||||
# Validate power panel
|
||||
if panel_name:
|
||||
try:
|
||||
self.instance.power_panel = PowerPanel.objects.get(site=site, name=panel_name)
|
||||
except Rack.DoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
"Power panel {} not found in site {}".format(panel_name, site)
|
||||
)
|
||||
|
||||
# Validate rack
|
||||
if rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, rack_group=rack_group, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
"Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
|
||||
)
|
||||
|
||||
|
||||
class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
powerpanel = forms.ModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites",
|
||||
filter_for={
|
||||
'rackgroup': 'site_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/racks",
|
||||
)
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
supply = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
phase = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
voltage = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
amperage = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
power_factor = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
comments = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'rackgroup', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = PowerFeed
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'rack_id': 'site',
|
||||
}
|
||||
)
|
||||
)
|
||||
rack_id = FilterChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/racks/",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=POWERFEED_STATUS_CHOICES,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
supply = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
phase = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
voltage = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
amperage = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
power_factor = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0069_deprecate_nullablecharfield'),
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
133
netbox/dcim/migrations/0072_powerfeeds.py
Normal file
133
netbox/dcim/migrations/0072_powerfeeds.py
Normal file
@ -0,0 +1,133 @@
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0021_add_color_comments_changelog_to_tag'),
|
||||
('dcim', '0071_device_components_add_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PowerFeed',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('status', models.PositiveSmallIntegerField(default=1)),
|
||||
('type', models.PositiveSmallIntegerField(default=1)),
|
||||
('supply', models.PositiveSmallIntegerField(default=1)),
|
||||
('phase', models.PositiveSmallIntegerField(default=1)),
|
||||
('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('power_factor', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('cable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['power_panel', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerPanel',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('rack_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='power_panel',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='rack',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='connected_endpoint',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerPort'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='powerport',
|
||||
old_name='connected_endpoint',
|
||||
new_name='_connected_poweroutlet',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='_connected_powerfeed',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerFeed'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='allocated_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='maximum_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='allocated_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='maximum_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerpanel',
|
||||
unique_together={('site', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerfeed',
|
||||
unique_together={('power_panel', 'name')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='feed_leg',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='power_port',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlets', to='dcim.PowerPort'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='feed_leg',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='power_port',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlet_templates', to='dcim.PowerPortTemplate'),
|
||||
),
|
||||
]
|
@ -9,7 +9,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
@ -1053,6 +1053,18 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
maximum_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Maximum current draw (watts)"
|
||||
)
|
||||
allocated_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Allocated current draw (watts)"
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
|
||||
@ -1076,6 +1088,19 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
power_port = models.ForeignKey(
|
||||
to='dcim.PowerPortTemplate',
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='poweroutlet_templates'
|
||||
)
|
||||
feed_leg = models.PositiveSmallIntegerField(
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
|
||||
@ -1086,6 +1111,14 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate power port assignment
|
||||
if self.power_port and self.power_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
"Parent power port ({}) must belong to the same device type".format(self.power_port)
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
@ -1828,13 +1861,32 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
connected_endpoint = models.OneToOneField(
|
||||
maximum_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Maximum current draw (watts)"
|
||||
)
|
||||
allocated_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Allocated current draw (watts)"
|
||||
)
|
||||
_connected_poweroutlet = models.OneToOneField(
|
||||
to='dcim.PowerOutlet',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='connected_endpoint',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_connected_powerfeed = models.OneToOneField(
|
||||
to='dcim.PowerFeed',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
@ -1843,7 +1895,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'description']
|
||||
csv_headers = ['device', 'name', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
@ -1859,9 +1911,68 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.maximum_draw,
|
||||
self.allocated_draw,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@property
|
||||
def connected_endpoint(self):
|
||||
if self._connected_poweroutlet:
|
||||
return self._connected_poweroutlet
|
||||
return self._connected_powerfeed
|
||||
|
||||
@connected_endpoint.setter
|
||||
def connected_endpoint(self, value):
|
||||
if value is None:
|
||||
self._connected_poweroutlet = None
|
||||
self._connected_powerfeed = None
|
||||
elif isinstance(value, PowerOutlet):
|
||||
self._connected_poweroutlet = value
|
||||
self._connected_powerfeed = None
|
||||
elif isinstance(value, PowerFeed):
|
||||
self._connected_poweroutlet = None
|
||||
self._connected_powerfeed = value
|
||||
else:
|
||||
raise ValueError(
|
||||
"Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
|
||||
)
|
||||
|
||||
def get_power_stats(self):
|
||||
"""
|
||||
Return power utilization statistics
|
||||
"""
|
||||
feed = self._connected_powerfeed
|
||||
if not feed or not self.poweroutlets.count():
|
||||
return None
|
||||
|
||||
stats = []
|
||||
powerfeed_available = self._connected_powerfeed.available_power
|
||||
|
||||
outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
|
||||
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
|
||||
maximum_draw=Sum('maximum_draw'),
|
||||
allocated_draw=Sum('allocated_draw'),
|
||||
)
|
||||
utilization['outlets'] = len(outlet_ids)
|
||||
utilization['available_power'] = powerfeed_available
|
||||
stats.append(utilization)
|
||||
|
||||
# Per-leg stats for three-phase feeds
|
||||
if feed.phase == POWERFEED_PHASE_3PHASE:
|
||||
for leg, leg_name in POWERFEED_LEG_CHOICES:
|
||||
outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
|
||||
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
|
||||
maximum_draw=Sum('maximum_draw'),
|
||||
allocated_draw=Sum('allocated_draw'),
|
||||
)
|
||||
utilization['name'] = 'Leg {}'.format(leg_name)
|
||||
utilization['outlets'] = len(outlet_ids)
|
||||
utilization['available_power'] = powerfeed_available / 3
|
||||
stats.append(utilization)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
@ -1879,6 +1990,19 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
power_port = models.ForeignKey(
|
||||
to='dcim.PowerPort',
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='poweroutlets'
|
||||
)
|
||||
feed_leg = models.PositiveSmallIntegerField(
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
@ -1887,7 +2011,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'description']
|
||||
csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
@ -1902,9 +2026,19 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.power_port.name if self.power_port else None,
|
||||
self.get_feed_leg_display(),
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate power port assignment
|
||||
if self.power_port and self.power_port.device != self.device:
|
||||
raise ValidationError(
|
||||
"Parent power port ({}) must belong to the same device".format(self.power_port)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
@ -2646,6 +2780,14 @@ class Cable(ChangeLoggedModel):
|
||||
def get_status_class(self):
|
||||
return 'success' if self.status else 'info'
|
||||
|
||||
def get_compatible_types(self):
|
||||
"""
|
||||
Return all termination types compatible with termination A.
|
||||
"""
|
||||
if self.termination_a is None:
|
||||
return
|
||||
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
||||
|
||||
def get_path_endpoints(self):
|
||||
"""
|
||||
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
||||
@ -2668,3 +2810,174 @@ class Cable(ChangeLoggedModel):
|
||||
b_endpoint = b_path[-1][2]
|
||||
|
||||
return a_endpoint, b_endpoint, path_status
|
||||
|
||||
|
||||
#
|
||||
# Power
|
||||
#
|
||||
|
||||
class PowerPanel(ChangeLoggedModel):
|
||||
"""
|
||||
A distribution point for electrical power; e.g. a data center RPP.
|
||||
"""
|
||||
site = models.ForeignKey(
|
||||
to='Site',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
rack_group = models.ForeignKey(
|
||||
to='RackGroup',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'rack_group_name', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = ['site', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerpanel', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.rack_group.name if self.rack_group else None,
|
||||
self.name,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# RackGroup must belong to assigned Site
|
||||
if self.rack_group and self.rack_group.site != self.site:
|
||||
raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
|
||||
self.rack_group, self.rack_group.site, self.site
|
||||
))
|
||||
|
||||
|
||||
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
"""
|
||||
An electrical circuit delivered from a PowerPanel.
|
||||
"""
|
||||
power_panel = models.ForeignKey(
|
||||
to='PowerPanel',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='powerfeeds'
|
||||
)
|
||||
rack = models.ForeignKey(
|
||||
to='Rack',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
connected_endpoint = models.OneToOneField(
|
||||
to='dcim.PowerPort',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
choices=POWERFEED_STATUS_CHOICES,
|
||||
default=POWERFEED_STATUS_ACTIVE
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=POWERFEED_TYPE_CHOICES,
|
||||
default=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
supply = models.PositiveSmallIntegerField(
|
||||
choices=POWERFEED_SUPPLY_CHOICES,
|
||||
default=POWERFEED_SUPPLY_AC
|
||||
)
|
||||
phase = models.PositiveSmallIntegerField(
|
||||
choices=POWERFEED_PHASE_CHOICES,
|
||||
default=POWERFEED_PHASE_SINGLE
|
||||
)
|
||||
voltage = models.PositiveSmallIntegerField(
|
||||
validators=[MinValueValidator(1)],
|
||||
default=120
|
||||
)
|
||||
amperage = models.PositiveSmallIntegerField(
|
||||
validators=[MinValueValidator(1)],
|
||||
default=20
|
||||
)
|
||||
power_factor = models.PositiveSmallIntegerField(
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
||||
default=80,
|
||||
help_text="Maximum permissible draw (percentage)"
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
custom_field_values = GenericRelation(
|
||||
to='extras.CustomFieldValue',
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'power_factor', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['power_panel', 'name']
|
||||
unique_together = ['power_panel', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerfeed', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.power_panel.name,
|
||||
self.rack.name if self.rack else None,
|
||||
self.name,
|
||||
self.get_status_display(),
|
||||
self.get_type_display(),
|
||||
self.get_supply_display(),
|
||||
self.get_phase_display(),
|
||||
self.voltage,
|
||||
self.amperage,
|
||||
self.power_factor,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Rack must belong to same Site as PowerPanel
|
||||
if self.rack and self.rack.site != self.power_panel.site:
|
||||
raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
|
||||
self.rack, self.rack.site, self.power_panel, self.power_panel.site
|
||||
))
|
||||
|
||||
def get_type_class(self):
|
||||
return STATUS_CLASSES[self.type]
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CLASSES[self.status]
|
||||
|
||||
@property
|
||||
def available_power(self):
|
||||
kva = self.voltage * self.amperage * self.power_factor
|
||||
if self.phase == POWERFEED_PHASE_3PHASE:
|
||||
return kva * 1.732
|
||||
return kva
|
||||
|
@ -6,8 +6,9 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
@ -144,6 +145,10 @@ STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
|
||||
TYPE_LABEL = """
|
||||
<span class="label label-{{ record.get_type_class }}">{{ record.get_type_display }}</span>
|
||||
"""
|
||||
|
||||
DEVICE_PRIMARY_IP = """
|
||||
{{ record.primary_ip6.address.ip|default:"" }}
|
||||
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
|
||||
@ -786,3 +791,50 @@ class VirtualChassisTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site.slug')]
|
||||
)
|
||||
powerfeed_count = tables.Column(
|
||||
verbose_name='Feeds'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
|
||||
|
||||
|
||||
#
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
class PowerFeedTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
power_panel = tables.LinkColumn(
|
||||
viewname='dcim:powerpanel',
|
||||
args=[Accessor('power_panel.pk')],
|
||||
)
|
||||
rack = tables.LinkColumn(
|
||||
viewname='dcim:rack',
|
||||
args=[Accessor('rack.pk')]
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
type = tables.TemplateColumn(
|
||||
template_code=TYPE_LABEL
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerFeed
|
||||
fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase')
|
||||
|
@ -7,8 +7,8 @@ from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, Interface, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
)
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
@ -3532,3 +3532,260 @@ class VirtualChassisTest(APITestCase):
|
||||
self.assertTrue(
|
||||
Device.objects.filter(pk=d.pk, virtual_chassis=None, vc_position=None, vc_priority=None)
|
||||
)
|
||||
|
||||
|
||||
class PowerPanelTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
|
||||
self.rackgroup2 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 2', slug='test-rack-group-2')
|
||||
self.rackgroup3 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 3', slug='test-rack-group-3')
|
||||
self.powerpanel1 = PowerPanel.objects.create(
|
||||
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 1'
|
||||
)
|
||||
self.powerpanel2 = PowerPanel.objects.create(
|
||||
site=self.site1, rack_group=self.rackgroup2, name='Test Power Panel 2'
|
||||
)
|
||||
self.powerpanel3 = PowerPanel.objects.create(
|
||||
site=self.site1, rack_group=self.rackgroup3, name='Test Power Panel 3'
|
||||
)
|
||||
|
||||
def test_get_powerpanel(self):
|
||||
|
||||
url = reverse('dcim-api:powerpanel-detail', kwargs={'pk': self.powerpanel1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.powerpanel1.name)
|
||||
|
||||
def test_list_powerpanels(self):
|
||||
|
||||
url = reverse('dcim-api:powerpanel-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_powerpanels_brief(self):
|
||||
|
||||
url = reverse('dcim-api:powerpanel-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_powerpanel(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Power Panel 4',
|
||||
'site': self.site1.pk,
|
||||
'rack_group': self.rackgroup1.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:powerpanel-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(PowerPanel.objects.count(), 4)
|
||||
powerpanel4 = PowerPanel.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(powerpanel4.name, data['name'])
|
||||
self.assertEqual(powerpanel4.site_id, data['site'])
|
||||
self.assertEqual(powerpanel4.rack_group_id, data['rack_group'])
|
||||
|
||||
def test_create_powerpanel_bulk(self):
|
||||
|
||||
data = [
|
||||
{
|
||||
'name': 'Test Power Panel 4',
|
||||
'site': self.site1.pk,
|
||||
'rack_group': self.rackgroup1.pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Power Panel 5',
|
||||
'site': self.site1.pk,
|
||||
'rack_group': self.rackgroup2.pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Power Panel 6',
|
||||
'site': self.site1.pk,
|
||||
'rack_group': self.rackgroup3.pk,
|
||||
},
|
||||
]
|
||||
|
||||
url = reverse('dcim-api:powerpanel-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(PowerPanel.objects.count(), 6)
|
||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
||||
|
||||
def test_update_powerpanel(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Power Panel X',
|
||||
'rack_group': self.rackgroup2.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:powerpanel-detail', kwargs={'pk': self.powerpanel1.pk})
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(PowerPanel.objects.count(), 3)
|
||||
powerpanel1 = PowerPanel.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(powerpanel1.name, data['name'])
|
||||
self.assertEqual(powerpanel1.rack_group_id, data['rack_group'])
|
||||
|
||||
def test_delete_powerpanel(self):
|
||||
|
||||
url = reverse('dcim-api:powerpanel-detail', kwargs={'pk': self.powerpanel1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(PowerPanel.objects.count(), 2)
|
||||
|
||||
|
||||
class PowerFeedTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
|
||||
self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
|
||||
self.rack1 = Rack.objects.create(
|
||||
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 1', u_height=42,
|
||||
)
|
||||
self.rack2 = Rack.objects.create(
|
||||
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 2', u_height=42,
|
||||
)
|
||||
self.rack3 = Rack.objects.create(
|
||||
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 3', u_height=42,
|
||||
)
|
||||
self.rack4 = Rack.objects.create(
|
||||
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 4', u_height=42,
|
||||
)
|
||||
self.powerpanel1 = PowerPanel.objects.create(
|
||||
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 1'
|
||||
)
|
||||
self.powerpanel2 = PowerPanel.objects.create(
|
||||
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
|
||||
)
|
||||
self.powerfeed1 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
self.powerfeed2 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
|
||||
)
|
||||
self.powerfeed3 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
self.powerfeed4 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
|
||||
)
|
||||
self.powerfeed5 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
self.powerfeed6 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
|
||||
)
|
||||
|
||||
def test_get_powerfeed(self):
|
||||
|
||||
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.powerfeed1.name)
|
||||
|
||||
def test_list_powerfeeds(self):
|
||||
|
||||
url = reverse('dcim-api:powerfeed-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 6)
|
||||
|
||||
def test_list_powerfeeds_brief(self):
|
||||
|
||||
url = reverse('dcim-api:powerfeed-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_powerfeed(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Power Feed 4A',
|
||||
'power_panel': self.powerpanel1.pk,
|
||||
'rack': self.rack4.pk,
|
||||
'type': POWERFEED_TYPE_PRIMARY,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:powerfeed-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(PowerFeed.objects.count(), 7)
|
||||
powerfeed4 = PowerFeed.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(powerfeed4.name, data['name'])
|
||||
self.assertEqual(powerfeed4.power_panel_id, data['power_panel'])
|
||||
self.assertEqual(powerfeed4.rack_id, data['rack'])
|
||||
|
||||
def test_create_powerfeed_bulk(self):
|
||||
|
||||
data = [
|
||||
{
|
||||
'name': 'Test Power Feed 4A',
|
||||
'power_panel': self.powerpanel1.pk,
|
||||
'rack': self.rack4.pk,
|
||||
'type': POWERFEED_TYPE_PRIMARY,
|
||||
},
|
||||
{
|
||||
'name': 'Test Power Feed 4B',
|
||||
'power_panel': self.powerpanel1.pk,
|
||||
'rack': self.rack4.pk,
|
||||
'type': POWERFEED_TYPE_REDUNDANT,
|
||||
},
|
||||
]
|
||||
|
||||
url = reverse('dcim-api:powerfeed-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(PowerFeed.objects.count(), 8)
|
||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||
|
||||
def test_update_powerfeed(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Power Feed X',
|
||||
'rack': self.rack4.pk,
|
||||
'type': POWERFEED_TYPE_REDUNDANT,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(PowerFeed.objects.count(), 6)
|
||||
powerfeed1 = PowerFeed.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(powerfeed1.name, data['name'])
|
||||
self.assertEqual(powerfeed1.rack_id, data['rack'])
|
||||
self.assertEqual(powerfeed1.type, data['type'])
|
||||
|
||||
def test_delete_powerfeed(self):
|
||||
|
||||
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(PowerFeed.objects.count(), 5)
|
||||
|
@ -6,7 +6,8 @@ from secrets.views import secret_add
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
||||
PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
app_name = 'dcim'
|
||||
@ -161,7 +162,7 @@ urlpatterns = [
|
||||
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
url(r'^console-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
url(r'^console-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
@ -170,7 +171,7 @@ urlpatterns = [
|
||||
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
@ -181,7 +182,7 @@ urlpatterns = [
|
||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
url(r'^power-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
url(r'^power-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
@ -190,7 +191,7 @@ urlpatterns = [
|
||||
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
@ -202,7 +203,7 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
|
||||
@ -217,7 +218,7 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
url(r'^front-ports/(?P<pk>\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
url(r'^front-ports/(?P<pk>\d+)/delete/$', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
url(r'^front-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
@ -229,7 +230,7 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/delete/$', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
@ -279,4 +280,25 @@ urlpatterns = [
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
|
||||
# Power panels
|
||||
url(r'^power-panels/$', views.PowerPanelListView.as_view(), name='powerpanel_list'),
|
||||
url(r'^power-panels/add/$', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
|
||||
url(r'^power-panels/import/$', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
|
||||
url(r'^power-panels/delete/$', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
|
||||
url(r'^power-panels/(?P<pk>\d+)/$', views.PowerPanelView.as_view(), name='powerpanel'),
|
||||
url(r'^power-panels/(?P<pk>\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
|
||||
url(r'^power-panels/(?P<pk>\d+)/delete/$', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
|
||||
url(r'^power-panels/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
|
||||
|
||||
# Racks
|
||||
url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'),
|
||||
url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'),
|
||||
url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
|
||||
url(r'^power-feeds/edit/$', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
|
||||
url(r'^power-feeds/delete/$', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
|
||||
url(r'^power-feeds/(?P<pk>\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'),
|
||||
url(r'^power-feeds/(?P<pk>\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
|
||||
url(r'^power-feeds/(?P<pk>\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
|
||||
url(r'^power-feeds/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
||||
|
||||
]
|
||||
|
@ -3,6 +3,7 @@ import re
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F
|
||||
@ -10,6 +11,7 @@ from django.forms import modelformset_factory
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
@ -30,8 +32,9 @@ from . import filters, forms, tables
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@ -391,10 +394,12 @@ class RackView(View):
|
||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||
|
||||
reservations = RackReservation.objects.filter(rack=rack)
|
||||
power_feeds = PowerFeed.objects.filter(rack=rack).select_related('power_panel')
|
||||
|
||||
return render(request, 'dcim/rack.html', {
|
||||
'rack': rack,
|
||||
'reservations': reservations,
|
||||
'power_feeds': power_feeds,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
@ -910,7 +915,7 @@ class DeviceView(View):
|
||||
consoleserverports = device.consoleserverports.select_related('connected_endpoint__device', 'cable')
|
||||
|
||||
# Power ports
|
||||
power_ports = device.powerports.select_related('connected_endpoint__device', 'cable')
|
||||
power_ports = device.powerports.select_related('_connected_poweroutlet__device', 'cable')
|
||||
|
||||
# Power outlets
|
||||
poweroutlets = device.poweroutlets.select_related('connected_endpoint__device', 'cable')
|
||||
@ -1670,20 +1675,79 @@ class CableTraceView(View):
|
||||
})
|
||||
|
||||
|
||||
class CableCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View):
|
||||
permission_required = 'dcim.add_cable'
|
||||
model = Cable
|
||||
model_form = forms.CableCreateForm
|
||||
template_name = 'dcim/cable_connect.html'
|
||||
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
# Retrieve endpoint A based on the given type and PK
|
||||
termination_a_type = url_kwargs.get('termination_a_type')
|
||||
termination_a_id = url_kwargs.get('termination_a_id')
|
||||
obj.termination_a = termination_a_type.objects.get(pk=termination_a_id)
|
||||
termination_a_type = kwargs.get('termination_a_type')
|
||||
termination_a_id = kwargs.get('termination_a_id')
|
||||
|
||||
return obj
|
||||
termination_b_type_name = kwargs.get('termination_b_type')
|
||||
self.termination_b_type = ContentType.objects.get(model=termination_b_type_name.replace('-', ''))
|
||||
|
||||
self.obj = Cable(
|
||||
termination_a=termination_a_type.objects.get(pk=termination_a_id),
|
||||
termination_b_type=self.termination_b_type
|
||||
)
|
||||
self.form_class = {
|
||||
'console-port': forms.ConnectCableToConsolePortForm,
|
||||
'console-server-port': forms.ConnectCableToConsoleServerPortForm,
|
||||
'power-port': forms.ConnectCableToPowerPortForm,
|
||||
'power-outlet': forms.ConnectCableToPowerOutletForm,
|
||||
'interface': forms.ConnectCableToInterfaceForm,
|
||||
'front-port': forms.ConnectCableToFrontPortForm,
|
||||
'power-feed': forms.ConnectCableToPowerFeedForm,
|
||||
'circuit-termination': forms.ConnectCableToCircuitTerminationForm,
|
||||
}[termination_b_type_name]
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
# Parse initial data manually to avoid setting field values as lists
|
||||
initial_data = {k: request.GET[k] for k in request.GET}
|
||||
|
||||
form = self.form_class(instance=self.obj, initial=initial_data)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': self.obj,
|
||||
'obj_type': Cable._meta.verbose_name,
|
||||
'termination_b_type': self.termination_b_type.name,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(request, self.obj),
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
form = self.form_class(request.POST, request.FILES, instance=self.obj)
|
||||
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
|
||||
msg = 'Created cable <a href="{}">{}</a>'.format(
|
||||
obj.get_absolute_url(),
|
||||
escape(obj)
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
return_url = form.cleaned_data.get('return_url')
|
||||
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
|
||||
return redirect(return_url)
|
||||
else:
|
||||
return redirect(self.get_return_url(request, obj))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': self.obj,
|
||||
'obj_type': Cable._meta.verbose_name,
|
||||
'termination_b_type': self.termination_b_type.name,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(request, self.obj),
|
||||
})
|
||||
|
||||
|
||||
class CableEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
@ -1760,11 +1824,11 @@ class ConsoleConnectionsListView(ObjectListView):
|
||||
|
||||
class PowerConnectionsListView(ObjectListView):
|
||||
queryset = PowerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
'device', '_connected_poweroutlet__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
_connected_poweroutlet__isnull=False
|
||||
).order_by(
|
||||
'cable', 'connected_endpoint__device__name', 'connected_endpoint__name'
|
||||
'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name'
|
||||
)
|
||||
filter = filters.PowerConnectionFilter
|
||||
filter_form = forms.PowerConnectionFilterForm
|
||||
@ -2114,3 +2178,139 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(request, device),
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelListView(ObjectListView):
|
||||
queryset = PowerPanel.objects.select_related(
|
||||
'site', 'rack_group'
|
||||
).annotate(
|
||||
powerfeed_count=Count('powerfeeds')
|
||||
)
|
||||
filter = filters.PowerPanelFilter
|
||||
filter_form = forms.PowerPanelFilterForm
|
||||
table = tables.PowerPanelTable
|
||||
template_name = 'dcim/powerpanel_list.html'
|
||||
|
||||
|
||||
class PowerPanelView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
powerpanel = get_object_or_404(PowerPanel.objects.select_related('site', 'rack_group'), pk=pk)
|
||||
powerfeed_table = tables.PowerFeedTable(
|
||||
data=PowerFeed.objects.filter(power_panel=powerpanel).select_related('rack'),
|
||||
orderable=False
|
||||
)
|
||||
powerfeed_table.exclude = ['power_panel']
|
||||
|
||||
return render(request, 'dcim/powerpanel.html', {
|
||||
'powerpanel': powerpanel,
|
||||
'powerfeed_table': powerfeed_table,
|
||||
})
|
||||
|
||||
|
||||
class PowerPanelCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.add_powerpanel'
|
||||
model = PowerPanel
|
||||
model_form = forms.PowerPanelForm
|
||||
default_return_url = 'dcim:powerpanel_list'
|
||||
|
||||
|
||||
class PowerPanelEditView(PowerPanelCreateView):
|
||||
permission_required = 'dcim.change_powerpanel'
|
||||
|
||||
|
||||
class PowerPanelDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_powerpanel'
|
||||
model = PowerPanel
|
||||
default_return_url = 'dcim:powerpanel_list'
|
||||
|
||||
|
||||
class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_powerpanel'
|
||||
model_form = forms.PowerPanelCSVForm
|
||||
table = tables.PowerPanelTable
|
||||
default_return_url = 'dcim:powerpanel_list'
|
||||
|
||||
|
||||
class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerpanel'
|
||||
queryset = PowerPanel.objects.select_related(
|
||||
'site', 'rack_group'
|
||||
).annotate(
|
||||
rack_count=Count('powerfeeds')
|
||||
)
|
||||
filter = filters.PowerPanelFilter
|
||||
table = tables.PowerPanelTable
|
||||
default_return_url = 'dcim:powerpanel_list'
|
||||
|
||||
|
||||
#
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
class PowerFeedListView(ObjectListView):
|
||||
queryset = PowerFeed.objects.select_related(
|
||||
'power_panel', 'rack'
|
||||
)
|
||||
filter = filters.PowerFeedFilter
|
||||
filter_form = forms.PowerFeedFilterForm
|
||||
table = tables.PowerFeedTable
|
||||
template_name = 'dcim/powerfeed_list.html'
|
||||
|
||||
|
||||
class PowerFeedView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
powerfeed = get_object_or_404(PowerFeed.objects.select_related('power_panel', 'rack'), pk=pk)
|
||||
|
||||
return render(request, 'dcim/powerfeed.html', {
|
||||
'powerfeed': powerfeed,
|
||||
})
|
||||
|
||||
|
||||
class PowerFeedCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.add_powerfeed'
|
||||
model = PowerFeed
|
||||
model_form = forms.PowerFeedForm
|
||||
template_name = 'dcim/powerfeed_edit.html'
|
||||
default_return_url = 'dcim:powerfeed_list'
|
||||
|
||||
|
||||
class PowerFeedEditView(PowerFeedCreateView):
|
||||
permission_required = 'dcim.change_powerfeed'
|
||||
|
||||
|
||||
class PowerFeedDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_powerfeed'
|
||||
model = PowerFeed
|
||||
default_return_url = 'dcim:powerfeed_list'
|
||||
|
||||
|
||||
class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_powerfeed'
|
||||
model_form = forms.PowerFeedCSVForm
|
||||
table = tables.PowerFeedTable
|
||||
default_return_url = 'dcim:powerfeed_list'
|
||||
|
||||
|
||||
class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_powerfeed'
|
||||
queryset = PowerFeed.objects.select_related('power_panel', 'rack')
|
||||
filter = filters.PowerFeedFilter
|
||||
table = tables.PowerFeedTable
|
||||
form = forms.PowerFeedBulkEditForm
|
||||
default_return_url = 'dcim:powerfeed_list'
|
||||
|
||||
|
||||
class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerfeed'
|
||||
queryset = PowerFeed.objects.select_related('power_panel', 'rack')
|
||||
filter = filters.PowerFeedFilter
|
||||
table = tables.PowerFeedTable
|
||||
default_return_url = 'dcim:powerfeed_list'
|
||||
|
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0017_exporttemplate_mime_type_length'),
|
||||
('extras', '0018_exporttemplate_add_jinja2'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -48,7 +48,7 @@ def delete_taggit_tags(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
('circuits', '0015_custom_tag_models'),
|
||||
('dcim', '0070_custom_tag_models'),
|
||||
('ipam', '0025_custom_tag_models'),
|
@ -7,7 +7,7 @@ import utilities.fields
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0019_tag_data'),
|
||||
('extras', '0020_tag_data'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -566,7 +566,7 @@ class TopologyMap(models.Model):
|
||||
from dcim.models import PowerPort
|
||||
|
||||
# Add all power connections to the graph
|
||||
for pp in PowerPort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
|
||||
for pp in PowerPort.objects.filter(device__in=devices, _connected_poweroutlet__device__in=devices):
|
||||
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)
|
||||
|
||||
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0024_vrf_allow_null_rd'),
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -166,7 +166,7 @@ class HomeView(View):
|
||||
connected_endpoint__isnull=False
|
||||
)
|
||||
connected_powerports = PowerPort.objects.filter(
|
||||
connected_endpoint__isnull=False
|
||||
_connected_poweroutlet__isnull=False
|
||||
)
|
||||
connected_interfaces = Interface.objects.filter(
|
||||
_connected_interface__isnull=False,
|
||||
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('secrets', '0005_change_logging'),
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with termination_a=form.instance.termination_a %}
|
||||
<h3>{% block title %}Connect {{ termination_a.device }} {{ termination_a }}{% endblock %}</h3>
|
||||
<h3>{% block title %}Connect {{ termination_a.device }} {{ termination_a }} to {{ termination_b_type|bettertitle }}{% endblock %}</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
@ -101,21 +101,43 @@
|
||||
<strong>B Side</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="search">
|
||||
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.termination_b_site %}
|
||||
{% render_field form.termination_b_rack %}
|
||||
{% if tabs %}
|
||||
<ul class="nav nav-tabs">
|
||||
{% for url, link in tabs %}
|
||||
<li role="presentation"><a href="{{ url }}">{{ link }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if 'termination_b_provider' in form.fields %}
|
||||
{% render_field form.termination_b_provider %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_site' in form.fields %}
|
||||
{% render_field form.termination_b_site %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_rackgroup' in form.fields %}
|
||||
{% render_field form.termination_b_rackgroup %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_rack' in form.fields %}
|
||||
{% render_field form.termination_b_rack %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_device' in form.fields %}
|
||||
{% render_field form.termination_b_device %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_type' in form.fields %}
|
||||
{% render_field form.termination_b_type %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_powerpanel' in form.fields %}
|
||||
{% render_field form.termination_b_powerpanel %}
|
||||
{% endif %}
|
||||
{% if 'termination_b_circuit' in form.fields %}
|
||||
{% render_field form.termination_b_circuit %}
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Type</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ termination_b_type|capfirst }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.termination_b_device %}
|
||||
{% render_field form.termination_b_type %}
|
||||
{% render_field form.termination_b_id %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -332,6 +332,49 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if power_ports and poweroutlets %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Utilization</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<th>Input</th>
|
||||
<th>Outlets</th>
|
||||
<th>Allocated/Max (W)</th>
|
||||
<th>Available (VA)</th>
|
||||
</tr>
|
||||
{% for pp in power_ports %}
|
||||
{% for leg in pp.get_power_stats %}
|
||||
<tr>
|
||||
{% if leg.name %}
|
||||
<td style="padding-left: 20px">{{ leg.name }}</td>
|
||||
{% else %}
|
||||
<td>{{ pp }}</td>
|
||||
{% endif %}
|
||||
<td>{{ leg.outlets|placeholder }}</td>
|
||||
<td>{{ leg.allocated_draw }} / {{ leg.maximum_draw }}</td>
|
||||
<td>{{ leg.available_power }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@ -627,9 +670,10 @@
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Input/Leg</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th colspan="3">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -4,6 +4,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||
</td>
|
||||
<td></td>
|
||||
|
||||
{# Description #}
|
||||
<td>
|
||||
@ -38,9 +39,16 @@
|
||||
{% if cp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=cp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:consoleport_connect' termination_a_id=cp.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=cp.pk termination_b_type='console-server-port' %}?return_url={{ device.get_absolute_url }}">Console Server Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=cp.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=cp.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
|
@ -47,9 +47,16 @@
|
||||
{% if csp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=csp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:consoleserverport_connect' termination_a_id=csp.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=csp.pk termination_b_type='console-port' %}?return_url={{ device.get_absolute_url }}">Console Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=csp.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=csp.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_consoleserverport %}
|
||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
|
@ -58,9 +58,17 @@
|
||||
{% if frontport.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=frontport.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:frontport_connect' termination_a_id=frontport.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=frontport.pk termination_b_type='interface' %}?return_url={{ device.get_absolute_url }}">Interface</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=frontport.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=frontport.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=frontport.pk termination_b_type='circuit-termination' %}?return_url={{ device.get_absolute_url }}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_frontport %}
|
||||
<a href="{% url 'dcim:frontport_edit' pk=frontport.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
|
@ -151,9 +151,17 @@
|
||||
{% if iface.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=iface.cable %}
|
||||
{% elif iface.is_connectable and perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk termination_b_type='interface' %}?return_url={{ device.get_absolute_url }}">Interface</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk termination_b_type='circuit-termination' %}?return_url={{ device.get_absolute_url }}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
|
@ -14,6 +14,15 @@
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||
</td>
|
||||
|
||||
{# Power port #}
|
||||
<td>
|
||||
{% if po.power_port %}
|
||||
{{ po.power_port }}{% if po.feed_leg %} / {{ po.get_feed_leg_display }}{% endif %}
|
||||
{% else %}
|
||||
<span class="text-warning">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>
|
||||
{{ po.description|placeholder }}
|
||||
@ -30,14 +39,23 @@
|
||||
|
||||
{# Connection #}
|
||||
{% if po.connected_endpoint %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=po.connected_endpoint.device.pk %}">{{ po.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ po.connected_endpoint }}
|
||||
</td>
|
||||
{% with pp=po.connected_endpoint %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=pp.device.pk %}">{{ pp.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ pp }}
|
||||
</td>
|
||||
<td>
|
||||
{% if pp.allocated_draw %}
|
||||
{{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %}
|
||||
{% elif pp.maximum_draw %}
|
||||
{{ pp.maximum_draw }}W
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
@ -47,7 +65,7 @@
|
||||
{% if po.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=po.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=po.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=po.pk termination_b_type='power-outlet' %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -5,6 +5,15 @@
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||
</td>
|
||||
|
||||
{# Current draw #}
|
||||
<td>
|
||||
{% if pp.allocated_draw %}
|
||||
{{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %}
|
||||
{% elif pp.maximum_draw %}
|
||||
{{ pp.maximum_draw }}W
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>
|
||||
{{ pp.description }}
|
||||
@ -20,13 +29,17 @@
|
||||
</td>
|
||||
|
||||
{# Connection #}
|
||||
{% if pp.connected_endpoint %}
|
||||
{% if pp.connected_endpoint.device %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=pp.connected_endpoint.device.pk %}">{{ pp.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ pp.connected_endpoint }}
|
||||
</td>
|
||||
{% elif pp.connected_endpoint %}
|
||||
<td colspan="2">
|
||||
<a href="{{ pp.connected_endpoint.get_absolute_url }}">{{ pp.connected_endpoint }}</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<span class="text-muted">Not connected</span>
|
||||
@ -38,9 +51,15 @@
|
||||
{% if pp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=pp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:powerport_connect' termination_a_id=pp.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:powerport_connect' termination_a_id=pp.pk termination_b_type='power-outlet' %}?return_url={{ device.get_absolute_url }}">Power Outlet</a></li>
|
||||
<li><a href="{% url 'dcim:powerport_connect' termination_a_id=pp.pk termination_b_type='power-feed' %}?return_url={{ device.get_absolute_url }}">Power Feed</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_powerport %}
|
||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
|
@ -57,9 +57,17 @@
|
||||
{% if rearport.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=rearport.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:rearport_connect' termination_a_id=rearport.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=rearport.pk termination_b_type='interface' %}?return_url={{ device.get_absolute_url }}">Interface</a></li>
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=rearport.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=rearport.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=rearport.pk termination_b_type='circuit-termination' %}?return_url={{ device.get_absolute_url }}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_rearport %}
|
||||
<a href="{% url 'dcim:rearport_edit' pk=rearport.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
|
131
netbox/templates/dcim/powerfeed.html
Normal file
131
netbox/templates/dcim/powerfeed.html
Normal file
@ -0,0 +1,131 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load tz %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a></li>
|
||||
<li><a href="{{ powerfeed.power_panel.site.get_absolute_url }}">{{ powerfeed.power_panel.site }}</a></li>
|
||||
<li><a href="{{ powerfeed.power_panel.get_absolute_url }}">{{ powerfeed.power_panel }}</a></li>
|
||||
{% if powerfeed.rack %}
|
||||
<li><a href="{{ powerfeed.rack.get_absolute_url }}">{{ powerfeed.rack }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ powerfeed }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:powerfeed_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search power feeds" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_powerfeed %}
|
||||
<a href="{% url 'dcim:powerfeed_edit' pk=powerfeed.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this power feed
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_powerfeed %}
|
||||
<a href="{% url 'dcim:powerfeed_delete' pk=powerfeed.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this power feed
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ powerfeed }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=powerfeed %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Feed</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Power Panel</td>
|
||||
<td>
|
||||
<a href="{{ powerfeed.power_panel.get_absolute_url }}">{{ powerfeed.power_panel }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>
|
||||
{% if powerfeed.rack %}
|
||||
<a href="{{ powerfeed.rack.get_absolute_url }}">{{ powerfeed.rack }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
<span class="label label-{{ powerfeed.get_type_class }}">{{ powerfeed.get_type_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ powerfeed.get_status_class }}">{{ powerfeed.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connected Device</td>
|
||||
<td>
|
||||
{% if powerfeed.connected_endpoint %}
|
||||
<a href="{{ powerfeed.connected_endpoint.device.get_absolute_url }}">{{ powerfeed.connected_endpoint.device }}</a> ({{ powerfeed.connected_endpoint }})
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Electrical Characteristics</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Supply</td>
|
||||
<td>{{ powerfeed.get_supply_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Voltage</td>
|
||||
<td>{{ powerfeed.voltage }}V</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Amperage</td>
|
||||
<td>{{ powerfeed.amperage }}A</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phase</td>
|
||||
<td>{{ powerfeed.get_phase_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Factor</td>
|
||||
<td>{{ powerfeed.power_factor }}%</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
46
netbox/templates/dcim/powerfeed_edit.html
Normal file
46
netbox/templates/dcim/powerfeed_edit.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Power Feed</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.power_panel %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.status %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Characteristics</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.type %}
|
||||
{% render_field form.supply %}
|
||||
{% render_field form.voltage %}
|
||||
{% render_field form.amperage %}
|
||||
{% render_field form.phase %}
|
||||
{% render_field form.power_factor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tags</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Comments</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
22
netbox/templates/dcim/powerfeed_list.html
Normal file
22
netbox/templates/dcim/powerfeed_list.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.add_powerfeed %}
|
||||
{% add_button 'dcim:powerfeed_add' %}
|
||||
{% import_button 'dcim:powerfeed_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Power Feeds{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
80
netbox/templates/dcim/powerpanel.html
Normal file
80
netbox/templates/dcim/powerpanel.html
Normal file
@ -0,0 +1,80 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load tz %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a></li>
|
||||
<li><a href="{{ powerpanel.site.get_absolute_url }}">{{ powerpanel.site }}</a></li>
|
||||
{% if powerpanel.rack_group %}
|
||||
<li><a href="{{ powerpanel.rack_group.get_absolute_url }}">{{ powerpanel.rack_group }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ powerpanel }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:powerpanel_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search power panels" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_powerpanel %}
|
||||
<a href="{% url 'dcim:powerpanel_edit' pk=powerpanel.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this power panel
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_powerpanel %}
|
||||
<a href="{% url 'dcim:powerpanel_delete' pk=powerpanel.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this power panel
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ powerpanel }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=powerpanel %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Panel</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
<a href="{{ powerpanel.site.get_absolute_url }}">{{ powerpanel.site }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack Group</td>
|
||||
<td>
|
||||
{% if powerpanel.rack_group %}
|
||||
<a href="{{ powerpanel.rack_group.get_absolute_url }}">{{ powerpanel.rack_group }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
{% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
21
netbox/templates/dcim/powerpanel_list.html
Normal file
21
netbox/templates/dcim/powerpanel_list.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.add_powerpanel %}
|
||||
{% add_button 'dcim:powerpanel_add' %}
|
||||
{% import_button 'dcim:powerpanel_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Power Panels{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -190,47 +190,37 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Non-Racked Devices</strong>
|
||||
</div>
|
||||
{% if nonracked_devices %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% if power_feeds %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Feeds</strong>
|
||||
</div>
|
||||
<table class="table panel-body">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Panel</th>
|
||||
<th>Feed</th>
|
||||
<th>Status</th>
|
||||
<th>Type</th>
|
||||
<th>Parent</th>
|
||||
</tr>
|
||||
{% for device in nonracked_devices %}
|
||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||
{% for powerfeed in power_feeds %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
<a href="{{ powerfeed.power_panel.get_absolute_url }}">{{ powerfeed.power_panel.name }}</a>
|
||||
|
||||
<td>
|
||||
<a href="{{ powerfeed.get_absolute_url }}">{{ powerfeed.name }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type.display_name }}</td>
|
||||
<td>
|
||||
{% if device.parent_bay %}
|
||||
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
<span class="label label-{{ powerfeed.get_status_class }}">{{ powerfeed.get_status_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ powerfeed.get_type_class }}">{{ powerfeed.get_type_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_device %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a non-racked device
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Images</strong>
|
||||
@ -299,19 +289,62 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row col-md-6">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Rear</h4>
|
||||
<div class="col-md-6">
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Rear</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Non-Racked Devices</strong>
|
||||
</div>
|
||||
{% if nonracked_devices %}
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Type</th>
|
||||
<th>Parent</th>
|
||||
</tr>
|
||||
{% for device in nonracked_devices %}
|
||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type.display_name }}</td>
|
||||
<td>
|
||||
{% if device.parent_bay %}
|
||||
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_device %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a non-racked device
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -368,6 +368,29 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/power' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Power <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
{% if perms.dcim.add_powerfeed %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:powerfeed_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'dcim:powerfeed_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a>
|
||||
</li>
|
||||
<li>
|
||||
{% if perms.dcim.add_powerpanel %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:powerpanel_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'dcim:powerpanel_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0005_change_logging'),
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0008_virtualmachine_local_context_data'),
|
||||
('extras', '0018_tag_taggeditem'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
Loading…
Reference in New Issue
Block a user