Просмотр исходного кода

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>
Abhimanyu Saharan 2 лет назад
Родитель
Сommit
36f95f7842

+ 5 - 1
netbox/dcim/api/serializers.py

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

+ 0 - 1
netbox/dcim/fields.py

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

+ 1 - 1
netbox/dcim/filtersets.py

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

+ 6 - 2
netbox/dcim/forms/bulk_edit.py

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

+ 7 - 1
netbox/dcim/forms/bulk_import.py

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

+ 2 - 1
netbox/dcim/forms/filtersets.py

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

+ 3 - 2
netbox/dcim/forms/model_forms.py

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

+ 20 - 0
netbox/dcim/migrations/0180_powerfeed_tenant.py

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

+ 8 - 1
netbox/dcim/models/power.py

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

+ 7 - 4
netbox/dcim/tables/power.py

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

+ 67 - 3
netbox/dcim/tests/test_filtersets.py

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

+ 9 - 0
netbox/templates/dcim/powerfeed.html

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

+ 2 - 1
netbox/tenancy/views.py

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