diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py
index e53259e94..edb1bb267 100644
--- a/netbox/dcim/api/nested_serializers.py
+++ b/netbox/dcim/api/nested_serializers.py
@@ -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(serializers.ModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
+
+ class Meta:
+ model = PowerPanel
+ fields = ['id', 'url', 'name']
+
+
+class NestedPowerFeedSerializer(serializers.ModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
+
+ class Meta:
+ model = PowerFeed
+ fields = ['id', 'url', 'name']
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index d18c59be5..cbd0f7cb5 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -7,8 +7,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
@@ -587,3 +588,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',
+ 'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+ ]
diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py
index 006a61bad..fd55d9b05 100644
--- a/netbox/dcim/api/urls.py
+++ b/netbox/dcim/api/urls.py
@@ -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')
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index bcb53c090..4f5edbee8 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -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
@@ -534,6 +535,26 @@ class VirtualChassisViewSet(ModelViewSet):
serializer_class = serializers.VirtualChassisSerializer
+#
+# Power panels
+#
+
+class PowerPanelViewSet(ModelViewSet):
+ queryset = PowerPanel.objects.all()
+ serializer_class = serializers.PowerPanelSerializer
+ # filterset_class = filters.PowerPanelFilter
+
+
+#
+# Power feeds
+#
+
+class PowerFeedViewSet(ModelViewSet):
+ queryset = PowerFeed.objects.all()
+ serializer_class = serializers.PowerFeedSerializer
+ # filterset_class = filters.PowerFeedFilter
+
+
#
# Miscellaneous
#
diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
index e68493af6..0783b8648 100644
--- a/netbox/dcim/constants.py
+++ b/netbox/dcim/constants.py
@@ -448,8 +448,8 @@ RACK_DIMENSION_UNIT_CHOICES = (
POWERFEED_TYPE_PRIMARY = 1
POWERFEED_TYPE_REDUNDANT = 2
POWERFEED_TYPE_CHOICES = (
- (POWERFEED_TYPE_PRIMARY, 'AC'),
- (POWERFEED_TYPE_REDUNDANT, 'DC'),
+ (POWERFEED_TYPE_PRIMARY, 'Primary'),
+ (POWERFEED_TYPE_REDUNDANT, 'Redundant'),
)
POWERFEED_SUPPLY_AC = 1
POWERFEED_SUPPLY_DC = 2
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index 6fe54cbbe..7f0a3265c 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -3183,7 +3183,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm):
'site': APISelect(
api_url="/api/dcim/sites/",
filter_for={
- 'rackgroup': 'site_id',
+ 'rack_group': 'site_id',
}
),
}
@@ -3231,7 +3231,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm):
class Meta:
model = PowerFeed
fields = [
- 'site', 'power_panel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase',
+ 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'comments', 'tags',
]
widgets = {
@@ -3241,24 +3241,24 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm):
'rack': APISelect(
api_url="/api/dcim/racks/"
),
- 'type': StaticSelect2(),
'status': StaticSelect2(),
+ 'type': StaticSelect2(),
'supply': StaticSelect2(),
'phase': StaticSelect2(),
}
class PowerFeedCSVForm(forms.ModelForm):
- type = CSVChoiceField(
- choices=POWERFEED_TYPE_CHOICES,
- required=False,
- help_text='Primary or redundant'
- )
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,
@@ -3292,14 +3292,14 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
api_url="/api/dcim/rack-groups",
)
)
- type = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
+ status = forms.ChoiceField(
+ choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
required=False,
initial='',
widget=StaticSelect2()
)
- status = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
+ type = forms.ChoiceField(
+ choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
required=False,
initial='',
widget=StaticSelect2()
@@ -3310,18 +3310,18 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
initial='',
widget=StaticSelect2()
)
- voltage = forms.IntegerField(
- required=False
- )
- amperage = forms.IntegerField(
- required=False
- )
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
+ )
max_utilization = forms.IntegerField(
required=False
)
diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
index 13efa6b8e..021d4e1a1 100644
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -2730,18 +2730,22 @@ class PowerFeed(ChangeLoggedModel, CustomFieldModel):
name = models.CharField(
max_length=50
)
- type = models.PositiveSmallIntegerField(
- choices=POWERFEED_TYPE_CHOICES,
- default=POWERFEED_TYPE_PRIMARY
- )
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
@@ -2750,10 +2754,6 @@ class PowerFeed(ChangeLoggedModel, CustomFieldModel):
validators=[MinValueValidator(1)],
default=20
)
- phase = models.PositiveSmallIntegerField(
- choices=POWERFEED_PHASE_CHOICES,
- default=POWERFEED_PHASE_SINGLE
- )
max_utilization = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(100)],
default=80,
@@ -2771,7 +2771,7 @@ class PowerFeed(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem)
csv_headers = [
- 'power_panel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization',
+ 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
'comments',
]
@@ -2790,12 +2790,18 @@ class PowerFeed(ChangeLoggedModel, CustomFieldModel):
self.power_panel.name,
self.rack.name if self.rack else None,
self.name,
- self.get_type_display(),
self.get_status_display(),
+ self.get_type_display(),
self.get_supply_display(),
+ self.get_phase_display(),
self.voltage,
self.amperage,
- self.get_phase_display(),
self.max_utilization,
self.comments,
)
+
+ def get_type_class(self):
+ return STATUS_CLASSES[self.type]
+
+ def get_status_class(self):
+ return STATUS_CLASSES[self.status]
diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
index 8106d997a..532ee96c1 100644
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -145,6 +145,10 @@ STATUS_LABEL = """
{{ record.get_status_display }}
"""
+TYPE_LABEL = """
+{{ record.get_type_display }}
+"""
+
DEVICE_PRIMARY_IP = """
{{ record.primary_ip6.address.ip|default:"" }}
{% if record.primary_ip6 and record.primary_ip4 %}
{% endif %}
@@ -799,17 +803,10 @@ class PowerPanelTable(BaseTable):
powerfeed_count = tables.Column(
verbose_name='Feeds'
)
- actions = tables.TemplateColumn(
- template_code=RACKROLE_ACTIONS,
- attrs={
- 'td': {'class': 'text-right noprint'}
- },
- verbose_name=''
- )
class Meta(BaseTable.Meta):
model = PowerPanel
- fields = ('pk', 'name', 'site', 'rackgroup', 'powerfeed_count', 'actions')
+ fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
#
@@ -819,16 +816,18 @@ class PowerPanelTable(BaseTable):
class PowerFeedTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
- powerpanel = tables.LinkColumn(
+ power_panel = tables.LinkColumn(
viewname='dcim:powerpanel',
- args=[Accessor('powerpanel.pk')],
+ args=[Accessor('power_panel.pk')],
)
- rack = tables.LinkColumn(
- viewname='dcim:rack',
- accessor=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', 'powerpanel', 'rack', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase')
+ fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase')
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 6c1518fe3..a97ef576b 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -392,10 +392,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,
@@ -2123,9 +2125,9 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
class PowerPanelListView(ObjectListView):
queryset = PowerPanel.objects.select_related(
- 'site', 'rackgroup'
+ 'site', 'rack_group'
).annotate(
- rack_count=Count('powerfeeds')
+ powerfeed_count=Count('powerfeeds')
)
table = tables.PowerPanelTable
template_name = 'dcim/powerpanel_list.html'
@@ -2183,7 +2185,7 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class PowerFeedListView(ObjectListView):
queryset = PowerFeed.objects.select_related(
- 'powerpanel', 'rack'
+ 'power_panel', 'rack'
)
# filter = filters.PowerFeedFilter
# filter_form = forms.PowerFeedFilterForm
@@ -2229,7 +2231,7 @@ class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_powerfeed'
- queryset = PowerFeed.objects.select_related('powerpanel', 'rack')
+ queryset = PowerFeed.objects.select_related('power_panel', 'rack')
# filter = filters.PowerFeedFilter
table = tables.PowerFeedTable
form = forms.PowerFeedBulkEditForm
@@ -2238,7 +2240,7 @@ class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerfeed'
- queryset = PowerFeed.objects.select_related('powerpanel', 'rack')
+ queryset = PowerFeed.objects.select_related('power_panel', 'rack')
# filter = filters.PowerFeedFilter
table = tables.PowerFeedTable
default_return_url = 'dcim:powerfeed_list'
diff --git a/netbox/templates/dcim/powerfeed_edit.html b/netbox/templates/dcim/powerfeed_edit.html
index df34746f8..f4b3ada46 100644
--- a/netbox/templates/dcim/powerfeed_edit.html
+++ b/netbox/templates/dcim/powerfeed_edit.html
@@ -9,13 +9,13 @@
{% render_field form.power_panel %}
{% render_field form.rack %}
{% render_field form.name %}
- {% render_field form.type %}
{% render_field form.status %}
Characteristics
+ {% render_field form.type %}
{% render_field form.supply %}
{% render_field form.voltage %}
{% render_field form.amperage %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html
index 68ea75b6c..2a142ae6c 100644
--- a/netbox/templates/dcim/rack.html
+++ b/netbox/templates/dcim/rack.html
@@ -190,47 +190,37 @@
{% endif %}
-
-
- Non-Racked Devices
-
- {% if nonracked_devices %}
-
+ {% if power_feeds %}
+
+
+ Power Feeds
+
+
- Name |
- Role |
+ Panel |
+ Feed |
+ Status |
Type |
- Parent |
- {% for device in nonracked_devices %}
-
+ {% for powerfeed in power_feeds %}
+
- {{ device }}
+ {{ powerfeed.power_panel.name }}
+
+ |
+ {{ powerfeed.name }}
|
- {{ device.device_role }} |
- {{ device.device_type.display_name }} |
- {% if device.parent_bay %}
- {{ device.parent_bay }}
- {% else %}
- —
- {% endif %}
+ {{ powerfeed.get_status_display }}
+ |
+
+ {{ powerfeed.get_type_display }}
|
{% endfor %}
- {% else %}
-
None
- {% endif %}
- {% if perms.dcim.add_device %}
-
- {% endif %}
-
+
+ {% endif %}
Images
@@ -307,11 +297,52 @@
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
-
+ {% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
+
+
+ Non-Racked Devices
+
+ {% if nonracked_devices %}
+
+
+ Name |
+ Role |
+ Type |
+ Parent |
+
+ {% for device in nonracked_devices %}
+
+
+ {{ device }}
+ |
+ {{ device.device_role }} |
+ {{ device.device_type.display_name }} |
+
+ {% if device.parent_bay %}
+ {{ device.parent_bay }}
+ {% else %}
+ —
+ {% endif %}
+ |
+
+ {% endfor %}
+
+ {% else %}
+
None
+ {% endif %}
+ {% if perms.dcim.add_device %}
+
+ {% endif %}
+
{% endblock %}