Procházet zdrojové kódy

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 před 2 roky
rodič
revize
36f95f7842

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

@@ -1236,6 +1236,10 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
         choices=PowerFeedPhaseChoices,
         choices=PowerFeedPhaseChoices,
         default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
         default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
     )
     )
+    tenant = NestedTenantSerializer(
+        required=False,
+        allow_null=True
+    )
 
 
     class Meta:
     class Meta:
         model = PowerFeed
         model = PowerFeed
@@ -1243,5 +1247,5 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
             'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
             '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',
             'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
             'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
             '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
 from .lookups import PathContains
 
 
 __all__ = (
 __all__ = (
-    'ASNField',
     'MACAddressField',
     'MACAddressField',
     'PathField',
     'PathField',
     'WWNField',
     'WWNField',

+ 1 - 1
netbox/dcim/filtersets.py

@@ -1880,7 +1880,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
+class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='power_panel__site__region',
         field_name='power_panel__site__region',

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

@@ -754,6 +754,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         widget=BulkEditNullBooleanSelect
         widget=BulkEditNullBooleanSelect
     )
     )
+    tenant = DynamicModelChoiceField(
+        queryset=Tenant.objects.all(),
+        required=False
+    )
     description = forms.CharField(
     description = forms.CharField(
         max_length=200,
         max_length=200,
         required=False
         required=False
@@ -764,10 +768,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = PowerFeed
     model = PowerFeed
     fieldsets = (
     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'))
         ('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,
         required=False,
         help_text=_('Rack')
         help_text=_('Rack')
     )
     )
+    tenant = CSVModelChoiceField(
+        queryset=Tenant.objects.all(),
+        to_field_name='name',
+        required=False,
+        help_text=_('Assigned tenant')
+    )
     status = CSVChoiceField(
     status = CSVChoiceField(
         choices=PowerFeedStatusChoices,
         choices=PowerFeedStatusChoices,
         help_text=_('Operational status')
         help_text=_('Operational status')
@@ -1195,7 +1201,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
         model = PowerFeed
         model = PowerFeed
         fields = (
         fields = (
             'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
             '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):
     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)
     tag = TagFilterField(model)
 
 
 
 
-class PowerFeedFilterForm(NetBoxModelFilterSetForm):
+class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = PowerFeed
     model = PowerFeed
     fieldsets = (
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         (None, ('q', 'filter_id', 'tag')),
         ('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
         ('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')),
         ('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     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(
     power_panel = DynamicModelChoiceField(
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
         selector=True
         selector=True
@@ -626,13 +626,14 @@ class PowerFeedForm(NetBoxModelForm):
     fieldsets = (
     fieldsets = (
         ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
         ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
         ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
         ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
+        ('Tenancy', ('tenant_group', 'tenant')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = PowerFeed
         model = PowerFeed
         fields = [
         fields = [
             'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
             '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,
         default=0,
         editable=False
         editable=False
     )
     )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='power_feeds',
+        blank=True,
+        null=True
+    )
 
 
     clone_fields = (
     clone_fields = (
         'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
         'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
-        'max_utilization',
+        'max_utilization', 'tenant',
     )
     )
     prerequisite_models = (
     prerequisite_models = (
         'dcim.PowerPanel',
         'dcim.PowerPanel',

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

@@ -1,6 +1,6 @@
 import django_tables2 as tables
 import django_tables2 as tables
 from dcim.models import PowerFeed, PowerPanel
 from dcim.models import PowerFeed, PowerPanel
-from tenancy.tables import ContactsColumnMixin
+from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 
 
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
 
 
@@ -51,7 +51,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
 
 
 # We're not using PathEndpointTable for PowerFeed because power connections
 # We're not using PathEndpointTable for PowerFeed because power connections
 # cannot traverse pass-through ports.
 # cannot traverse pass-through ports.
-class PowerFeedTable(CableTerminationTable):
+class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -69,6 +69,9 @@ class PowerFeedTable(CableTerminationTable):
     available_power = tables.Column(
     available_power = tables.Column(
         verbose_name='Available power (VA)'
         verbose_name='Available power (VA)'
     )
     )
+    tenant = tables.Column(
+        linkify=True
+    )
     comments = columns.MarkdownColumn()
     comments = columns.MarkdownColumn()
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:powerfeed_list'
         url_name='dcim:powerfeed_list'
@@ -78,8 +81,8 @@ class PowerFeedTable(CableTerminationTable):
         model = PowerFeed
         model = PowerFeed
         fields = (
         fields = (
             'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
             '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 = (
         default_columns = (
             'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
             '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)
         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 = (
         power_panels = (
             PowerPanel(name='Power Panel 1', site=sites[0]),
             PowerPanel(name='Power Panel 1', site=sites[0]),
             PowerPanel(name='Power Panel 2', site=sites[1]),
             PowerPanel(name='Power Panel 2', site=sites[1]),
@@ -4427,9 +4442,44 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
         PowerPanel.objects.bulk_create(power_panels)
         PowerPanel.objects.bulk_create(power_panels)
 
 
         power_feeds = (
         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)
         PowerFeed.objects.bulk_create(power_feeds)
 
 
@@ -4520,6 +4570,20 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'connected': False}
         params = {'connected': False}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         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):
 class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = VirtualDeviceContext.objects.all()
     queryset = VirtualDeviceContext.objects.all()

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

@@ -43,6 +43,15 @@
                         <th scope="row">{% trans "Description" %}</th>
                         <th scope="row">{% trans "Description" %}</th>
                         <td>{{ object.description|placeholder }}</td>
                         <td>{{ object.description|placeholder }}</td>
                     </tr>
                     </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>
                     <tr>
                         <th scope="row">{% trans "Connected Device" %}</th>
                         <th scope="row">{% trans "Connected Device" %}</th>
                         <td>
                         <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 django.utils.translation import gettext as _
 
 
 from circuits.models import Circuit
 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 ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
 from netbox.views import generic
 from netbox.views import generic
 from utilities.utils import count_related
 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'),
             (Device.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
             (VirtualDeviceContext.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'),
             (Cable.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
+            (PowerFeed.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
             # IPAM
             # IPAM
             (VRF.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
             (VRF.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
             (Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
             (Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),