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

Adds tenant on power feed (#13300)

* adds tenant on power feed

* cleanup

* adds power feed count on tenant object view

* Misc cleanup; add filterset tests

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Abhimanyu Saharan
2023-07-31 17:50:48 +05:30
committed by GitHub
parent 07f68ae579
commit 36f95f7842
13 changed files with 137 additions and 18 deletions

View File

@ -1236,6 +1236,10 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
choices=PowerFeedPhaseChoices,
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
)
tenant = NestedTenantSerializer(
required=False,
allow_null=True
)
class Meta:
model = PowerFeed
@ -1243,5 +1247,5 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'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',
]

View File

@ -6,7 +6,6 @@ from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
from .lookups import PathContains
__all__ = (
'ASNField',
'MACAddressField',
'PathField',
'WWNField',

View File

@ -1880,7 +1880,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
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',

View File

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

View File

@ -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')
@ -1195,7 +1201,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
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):

View File

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

View File

@ -611,7 +611,7 @@ class PowerPanelForm(NetBoxModelForm):
]
class PowerFeedForm(NetBoxModelForm):
class PowerFeedForm(TenancyForm, NetBoxModelForm):
power_panel = DynamicModelChoiceField(
queryset=PowerPanel.objects.all(),
selector=True
@ -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'
]

View File

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

View File

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

View File

@ -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
@ -51,7 +51,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
# 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
)
@ -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'
@ -78,8 +81,8 @@ class PowerFeedTable(CableTerminationTable):
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',

View File

@ -4419,6 +4419,21 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
)
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]),
@ -4427,9 +4442,44 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
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)
@ -4520,6 +4570,20 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
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()

View File

@ -43,6 +43,15 @@
<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>

View File

@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404
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
@ -145,6 +145,7 @@ class TenantView(generic.ObjectView):
(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'),