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 +
+
- - + + + - - {% for device in nonracked_devices %} - + {% for powerfeed in power_feeds %} + - - + {% endfor %}
NameRolePanelFeedStatus TypeParent
- {{ 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 }}
- {% 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 %}
-
+

Rear

-
- {% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 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 %} + + + + + + + + {% for device in nonracked_devices %} + + + + + + + {% endfor %} +
NameRoleTypeParent
+ {{ device }} + {{ device.device_role }}{{ device.device_type.display_name }} + {% if device.parent_bay %} + {{ device.parent_bay }} + {% else %} + + {% endif %} +
+ {% else %} +
None
+ {% endif %} + {% if perms.dcim.add_device %} + + {% endif %} +
{% endblock %}