From 5b753923b61748c947877bd0db5569c906a3e895 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Mar 2019 22:40:52 -0400 Subject: [PATCH 01/21] Initial work on power modeling (WIP) --- netbox/dcim/constants.py | 30 ++++ netbox/dcim/forms.py | 170 ++++++++++++++++++++- netbox/dcim/migrations/0072_powerfeeds.py | 74 +++++++++ netbox/dcim/models.py | 131 ++++++++++++++++ netbox/dcim/tables.py | 50 +++++- netbox/dcim/urls.py | 22 ++- netbox/dcim/views.py | 115 +++++++++++++- netbox/templates/dcim/powerfeed.html | 111 ++++++++++++++ netbox/templates/dcim/powerfeed_edit.html | 45 ++++++ netbox/templates/dcim/powerfeed_list.html | 22 +++ netbox/templates/dcim/powerpanel_list.html | 18 +++ netbox/templates/inc/nav_menu.html | 23 +++ 12 files changed, 804 insertions(+), 7 deletions(-) create mode 100644 netbox/dcim/migrations/0072_powerfeeds.py create mode 100644 netbox/templates/dcim/powerfeed.html create mode 100644 netbox/templates/dcim/powerfeed_edit.html create mode 100644 netbox/templates/dcim/powerfeed_list.html create mode 100644 netbox/templates/dcim/powerpanel_list.html diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 0b81e68bf..e68493af6 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -443,3 +443,33 @@ 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, 'AC'), + (POWERFEED_TYPE_REDUNDANT, 'DC'), +) +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'), +) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ab3da181f..8e429137a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -26,8 +26,8 @@ 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+\}' @@ -3156,3 +3156,169 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --', ) + + +# +# Power panels +# + +class PowerPanelForm(BootstrapMixin, forms.ModelForm): + rackgroup = ChainedModelChoiceField( + queryset=RackGroup.objects.all(), + chains=( + ('site', 'site'), + ), + required=False, + widget=APISelect( + api_url='/api/dcim/rack-groups/', + ) + ) + + class Meta: + model = PowerPanel + fields = [ + 'site', 'rackgroup', 'name', + ] + widgets = { + 'site': APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'rackgroup': '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.', + } + ) + group_name = forms.CharField( + help_text='Name of rack group', + required=False + ) + + class Meta: + model = PowerPanel + fields = PowerPanel.csv_headers + + +# +# Power feeds +# + +class PowerFeedForm(BootstrapMixin, CustomFieldForm): + tags = TagField( + required=False + ) + + class Meta: + model = PowerFeed + fields = [ + 'powerpanel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization', + 'comments', 'tags', + ] + widgets = { + 'site': APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'rackgroup': 'site_id', + } + ), + 'type': StaticSelect2(), + 'status': 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' + ) + supply = CSVChoiceField( + choices=POWERFEED_SUPPLY_CHOICES, + required=False, + help_text='AC/DC' + ) + + class Meta: + model = PowerFeed + fields = PowerFeed.csv_headers + + +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', + } + ) + ) + rackgroup = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/rack-groups", + ) + ) + type = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + required=False, + initial='', + widget=StaticSelect2() + ) + status = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_STATUS_CHOICES), + required=False, + initial='', + widget=StaticSelect2() + ) + supply = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + required=False, + 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() + ) + max_utilization = forms.IntegerField( + required=False + ) + comments = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = [ + 'rackgroup', 'comments', + ] diff --git a/netbox/dcim/migrations/0072_powerfeeds.py b/netbox/dcim/migrations/0072_powerfeeds.py new file mode 100644 index 000000000..ddfbb8ec1 --- /dev/null +++ b/netbox/dcim/migrations/0072_powerfeeds.py @@ -0,0 +1,74 @@ +# Generated by Django 2.1.7 on 2019-03-12 02:29 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0020_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)), + ('type', models.PositiveSmallIntegerField(default=1)), + ('status', models.PositiveSmallIntegerField(default=1)), + ('supply', 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)])), + ('phase', models.PositiveSmallIntegerField(default=1)), + ('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), + ('comments', models.TextField(blank=True)), + ], + options={ + 'ordering': ['powerpanel', '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)), + ('rackgroup', 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='powerpanel', + 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.AlterUniqueTogether( + name='powerpanel', + unique_together={('site', 'name')}, + ), + migrations.AlterUniqueTogether( + name='powerfeed', + unique_together={('powerpanel', 'name')}, + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index cbfce0b91..ad7e198fb 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2668,3 +2668,134 @@ 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 + ) + rackgroup = models.ForeignKey( + to='RackGroup', + on_delete=models.PROTECT, + blank=True, + null=True + ) + name = models.CharField( + max_length=50 + ) + + csv_headers = ['site', 'rackgroup', '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.rackgroup.name if self.rackgroup else None, + self.name, + ) + + +class PowerFeed(ChangeLoggedModel, CustomFieldModel): + """ + An electrical circuit delivered from a PowerPanel. + """ + powerpanel = models.ForeignKey( + to='PowerPanel', + on_delete=models.PROTECT, + related_name='powerfeeds' + ) + rack = models.ForeignKey( + to='Rack', + on_delete=models.PROTECT, + blank=True, + null=True + ) + 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 + ) + supply = models.PositiveSmallIntegerField( + choices=POWERFEED_SUPPLY_CHOICES, + default=POWERFEED_SUPPLY_AC + ) + voltage = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=120 + ) + amperage = models.PositiveSmallIntegerField( + 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, + 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 = [ + 'powerpanel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization', + 'comments', + ] + + class Meta: + ordering = ['powerpanel', 'name'] + unique_together = ['powerpanel', '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.powerpanel.name, + self.rack.name if self.rack else None, + self.name, + self.get_type_display(), + self.get_status_display(), + self.get_supply_display(), + self.voltage, + self.amperage, + self.get_phase_display(), + self.max_utilization, + self.comments, + ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 436b9053d..8106d997a 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -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 = """ @@ -786,3 +787,48 @@ 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() + 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') + + +# +# Power feeds +# + +class PowerFeedTable(BaseTable): + pk = ToggleColumn() + name = tables.LinkColumn() + powerpanel = tables.LinkColumn( + viewname='dcim:powerpanel', + args=[Accessor('powerpanel.pk')], + + ) + rack = tables.LinkColumn( + viewname='dcim:rack', + accessor=Accessor('rack.pk') + ) + + class Meta(BaseTable.Meta): + model = PowerFeed + fields = ('pk', 'name', 'powerpanel', 'rack', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 21d620af1..a087a0ae9 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -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' @@ -279,4 +280,23 @@ urlpatterns = [ url(r'^virtual-chassis/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), url(r'^virtual-chassis-members/(?P\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\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), + url(r'^power-panels/(?P\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\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'), + url(r'^power-feeds/(?P\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), + url(r'^power-feeds/(?P\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), + url(r'^power-feeds/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), + ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 27f90a3a2..958975e16 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -30,8 +30,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, ) @@ -2114,3 +2115,113 @@ 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', 'rackgroup' + ).annotate( + rack_count=Count('powerfeeds') + ) + table = tables.PowerPanelTable + template_name = 'dcim/powerpanel_list.html' + + +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 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') + ) + table = tables.PowerPanelTable + default_return_url = 'dcim:powerpanel_list' + + +# +# Power feeds +# + +class PowerFeedListView(ObjectListView): + queryset = PowerFeed.objects.select_related( + 'powerpanel', '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('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('powerpanel', '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('powerpanel', 'rack') + # filter = filters.PowerFeedFilter + table = tables.PowerFeedTable + default_return_url = 'dcim:powerfeed_list' diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html new file mode 100644 index 000000000..9dc4843f4 --- /dev/null +++ b/netbox/templates/dcim/powerfeed.html @@ -0,0 +1,111 @@ +{% extends '_base.html' %} +{% load static %} +{% load tz %} +{% load helpers %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if perms.dcim.change_powerfeed %} + + + Edit this power feed + + {% endif %} + {% if perms.dcim.delete_powerfeed %} + + + Delete this power feed + + {% endif %} +
+

{% block title %}{{ powerfeed }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=powerfeed %} +{% endblock %} + +{% block content %} +
+
+
+
+ Power Feed +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Power Panel + {{ powerfeed.powerpanel }} +
Rack + {% if powerfeed.rack %} + {{ powerfeed.rack }} + {% else %} + None + {% endif %} +
Type + {{ powerfeed.get_type_display }} +
Status + {{ powerfeed.get_status_display }} +
Supply{{ powerfeed.get_supply_display }}
Voltage{{ powerfeed.voltage }}V
Amperage{{ powerfeed.amperage }}A
Phase{{ powerfeed.get_phase_display }}
Max Utilization{{ powerfeed.max_utilization }}%
+
+
+
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/powerfeed_edit.html b/netbox/templates/dcim/powerfeed_edit.html new file mode 100644 index 000000000..709112f93 --- /dev/null +++ b/netbox/templates/dcim/powerfeed_edit.html @@ -0,0 +1,45 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Power Feed
+
+ {% render_field form.powerpanel %} + {% render_field form.rack %} + {% render_field form.name %} + {% render_field form.type %} + {% render_field form.status %} +
+
+
+
Characteristics
+
+ {% render_field form.supply %} + {% render_field form.voltage %} + {% render_field form.amperage %} + {% render_field form.phase %} + {% render_field form.max_utilization %} +
+
+ {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} +
+
Tags
+
+ {% render_field form.tags %} +
+
+
+
Comments
+
+ {% render_field form.comments %} +
+
+{% endblock %} diff --git a/netbox/templates/dcim/powerfeed_list.html b/netbox/templates/dcim/powerfeed_list.html new file mode 100644 index 000000000..cfe2c989c --- /dev/null +++ b/netbox/templates/dcim/powerfeed_list.html @@ -0,0 +1,22 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +
+ {% if perms.dcim.add_powerfeed %} + {% add_button 'dcim:powerfeed_add' %} + {% import_button 'dcim:powerfeed_import' %} + {% endif %} + {% export_button content_type %} +
+

{% block title %}Power Feeds{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %} +
+
+ {% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %} +
+
+{% endblock %} diff --git a/netbox/templates/dcim/powerpanel_list.html b/netbox/templates/dcim/powerpanel_list.html new file mode 100644 index 000000000..b58469c12 --- /dev/null +++ b/netbox/templates/dcim/powerpanel_list.html @@ -0,0 +1,18 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +
+ {% if perms.dcim.add_powerpanel %} + {% add_button 'dcim:powerpanel_add' %} + {% import_button 'dcim:powerpanel_import' %} + {% endif %} + {% export_button content_type %} +
+

{% block title %}Power Panels{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %} +
+
+{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 5f8f371d3..a08eb96e0 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -368,6 +368,29 @@ + {% if request.user.is_authenticated %}