Skip to content

Commit

Permalink
Adds tenant on power feed (#13300)
Browse files Browse the repository at this point in the history
* adds tenant on power feed

* cleanup

* adds power feed count on tenant object view

* Misc cleanup; add filterset tests

---------

Co-authored-by: Jeremy Stretch <[email protected]>
  • Loading branch information
abhi1693 and jeremystretch authored Jul 31, 2023
1 parent 07f68ae commit 36f95f7
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 18 deletions.
6 changes: 5 additions & 1 deletion netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1236,12 +1236,16 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
choices=PowerFeedPhaseChoices,
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
)
tenant = NestedTenantSerializer(
required=False,
allow_null=True
)

class Meta:
model = PowerFeed
fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
1 change: 0 additions & 1 deletion netbox/dcim/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from .lookups import PathContains

__all__ = (
'ASNField',
'MACAddressField',
'PathField',
'WWNField',
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1880,7 +1880,7 @@ def search(self, queryset, name, value):
return queryset.filter(qs_filter)


class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='power_panel__site__region',
Expand Down
8 changes: 6 additions & 2 deletions netbox/dcim/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
max_length=200,
required=False
Expand All @@ -764,10 +768,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):

model = PowerFeed
fieldsets = (
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description')),
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description', 'tenant')),
('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
)
nullable_fields = ('location', 'description', 'comments')
nullable_fields = ('location', 'tenant', 'description', 'comments')


#
Expand Down
8 changes: 7 additions & 1 deletion netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,12 @@ class PowerFeedImportForm(NetBoxModelImportForm):
required=False,
help_text=_('Rack')
)
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text=_('Assigned tenant')
)
status = CSVChoiceField(
choices=PowerFeedStatusChoices,
help_text=_('Operational status')
Expand All @@ -1195,7 +1201,7 @@ class Meta:
model = PowerFeed
fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags',
)

def __init__(self, data=None, *args, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,11 +985,12 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model)


class PowerFeedFilterForm(NetBoxModelFilterSetForm):
class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = PowerFeed
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
)
region_id = DynamicModelMultipleChoiceField(
Expand Down
5 changes: 3 additions & 2 deletions netbox/dcim/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ class Meta:
]


class PowerFeedForm(NetBoxModelForm):
class PowerFeedForm(TenancyForm, NetBoxModelForm):
power_panel = DynamicModelChoiceField(
queryset=PowerPanel.objects.all(),
selector=True
Expand All @@ -626,13 +626,14 @@ class PowerFeedForm(NetBoxModelForm):
fieldsets = (
('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
('Tenancy', ('tenant_group', 'tenant')),
)

class Meta:
model = PowerFeed
fields = [
'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'description', 'comments', 'tags',
'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
]


Expand Down
20 changes: 20 additions & 0 deletions netbox/dcim/migrations/0180_powerfeed_tenant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.1.8 on 2023-07-29 11:29

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('tenancy', '0010_tenant_relax_uniqueness'),
('dcim', '0179_interfacetemplate_rf_role'),
]

operations = [
migrations.AddField(
model_name='powerfeed',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='power_feeds', to='tenancy.tenant'),
),
]
9 changes: 8 additions & 1 deletion netbox/dcim/models/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,17 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
default=0,
editable=False
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='power_feeds',
blank=True,
null=True
)

clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization',
'max_utilization', 'tenant',
)
prerequisite_models = (
'dcim.PowerPanel',
Expand Down
11 changes: 7 additions & 4 deletions netbox/dcim/tables/power.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin

from netbox.tables import NetBoxTable, columns

Expand Down Expand Up @@ -51,7 +51,7 @@ class Meta(NetBoxTable.Meta):

# We're not using PathEndpointTable for PowerFeed because power connections
# cannot traverse pass-through ports.
class PowerFeedTable(CableTerminationTable):
class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
name = tables.Column(
linkify=True
)
Expand All @@ -69,6 +69,9 @@ class PowerFeedTable(CableTerminationTable):
available_power = tables.Column(
verbose_name='Available power (VA)'
)
tenant = tables.Column(
linkify=True
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:powerfeed_list'
Expand All @@ -78,8 +81,8 @@ class Meta(NetBoxTable.Meta):
model = PowerFeed
fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
'description', 'comments', 'tags', 'created', 'last_updated',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
Expand Down
70 changes: 67 additions & 3 deletions netbox/dcim/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4419,6 +4419,21 @@ def setUpTestData(cls):
)
Rack.objects.bulk_create(racks)

tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
)
for tenantgroup in tenant_groups:
tenantgroup.save()

tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)

power_panels = (
PowerPanel(name='Power Panel 1', site=sites[0]),
PowerPanel(name='Power Panel 2', site=sites[1]),
Expand All @@ -4427,9 +4442,44 @@ def setUpTestData(cls):
PowerPanel.objects.bulk_create(power_panels)

power_feeds = (
PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10),
PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20),
PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30),
PowerFeed(
power_panel=power_panels[0],
rack=racks[0],
name='Power Feed 1',
tenant=tenants[0],
status=PowerFeedStatusChoices.STATUS_ACTIVE,
type=PowerFeedTypeChoices.TYPE_PRIMARY,
supply=PowerFeedSupplyChoices.SUPPLY_AC,
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=100,
amperage=100,
max_utilization=10
),
PowerFeed(
power_panel=power_panels[1],
rack=racks[1],
name='Power Feed 2',
tenant=tenants[1],
status=PowerFeedStatusChoices.STATUS_FAILED,
type=PowerFeedTypeChoices.TYPE_PRIMARY,
supply=PowerFeedSupplyChoices.SUPPLY_AC,
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=200,
amperage=200,
max_utilization=20),
PowerFeed(
power_panel=power_panels[2],
rack=racks[2],
name='Power Feed 3',
tenant=tenants[2],
status=PowerFeedStatusChoices.STATUS_OFFLINE,
type=PowerFeedTypeChoices.TYPE_REDUNDANT,
supply=PowerFeedSupplyChoices.SUPPLY_DC,
phase=PowerFeedPhaseChoices.PHASE_SINGLE,
voltage=300,
amperage=300,
max_utilization=30
),
)
PowerFeed.objects.bulk_create(power_feeds)

Expand Down Expand Up @@ -4520,6 +4570,20 @@ def test_connected(self):
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)

def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)


class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDeviceContext.objects.all()
Expand Down
9 changes: 9 additions & 0 deletions netbox/templates/dcim/powerfeed.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ <h5 class="card-header">
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Connected Device" %}</th>
<td>
Expand Down
3 changes: 2 additions & 1 deletion netbox/tenancy/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.utils.translation import gettext as _

from circuits.models import Circuit
from dcim.models import Cable, Device, Location, Rack, RackReservation, Site, VirtualDeviceContext
from dcim.models import Cable, Device, Location, PowerFeed, Rack, RackReservation, Site, VirtualDeviceContext
from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
from netbox.views import generic
from utilities.utils import count_related
Expand Down Expand Up @@ -145,6 +145,7 @@ def get_extra_context(self, request, instance):
(Device.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
(VirtualDeviceContext.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
(Cable.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
(PowerFeed.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
# IPAM
(VRF.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
(Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
Expand Down

0 comments on commit 36f95f7

Please sign in to comment.