1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

More power work

This commit is contained in:
Jeremy Stretch
2019-03-12 11:36:29 -04:00
parent 3b9c0e4c67
commit e06dece00c
11 changed files with 230 additions and 91 deletions

View File

@ -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']

View File

@ -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',
]

View File

@ -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')

View File

@ -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
#

View File

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

View File

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

View File

@ -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]

View File

@ -145,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 %}
@ -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')

View File

@ -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'

View File

@ -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 %}
</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 %}

View File

@ -190,47 +190,37 @@
{% endif %}
</div>
</div>
{% if power_feeds %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Non-Racked Devices</strong>
<strong>Power Feeds</strong>
</div>
{% if nonracked_devices %}
<table class="table table-hover panel-body">
<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">&mdash;</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 class="panel panel-default">
<div class="panel-heading">
<strong>Images</strong>
@ -312,6 +302,47 @@
</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 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">&mdash;</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>
</div>
</div>
{% endblock %}