mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge pull request #18748 from netbox-community/18352-add-poweroutlet-status
Closes #18352: Adds PowerOutlet.status field
This commit is contained in:
commit
7c52698c08
@ -29,6 +29,19 @@ An alternative physical label identifying the power outlet.
|
||||
|
||||
The type of power outlet.
|
||||
|
||||
### Status
|
||||
|
||||
The operational status of the power outlet. By default, the following statuses are available:
|
||||
|
||||
* Enabled
|
||||
* Disabled
|
||||
* Faulty
|
||||
|
||||
!!! tip "Custom power outlet statuses"
|
||||
Additional power outlet statuses may be defined by setting `PowerOutlet.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.3."
|
||||
|
||||
### Color
|
||||
|
||||
!!! info "This field was introduced in NetBox v4.2."
|
||||
|
@ -156,10 +156,10 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port',
|
||||
'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
|
||||
'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||
|
||||
|
@ -1627,6 +1627,23 @@ class PowerFeedPhaseChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# PowerOutlets
|
||||
#
|
||||
class PowerOutletStatusChoices(ChoiceSet):
|
||||
key = 'PowerOutlet.status'
|
||||
|
||||
STATUS_ENABLED = 'enabled'
|
||||
STATUS_DISABLED = 'disabled'
|
||||
STATUS_FAULTY = 'faulty'
|
||||
|
||||
CHOICES = [
|
||||
(STATUS_ENABLED, _('Enabled'), 'green'),
|
||||
(STATUS_DISABLED, _('Disabled'), 'red'),
|
||||
(STATUS_FAULTY, _('Faulty'), 'gray'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# VDC
|
||||
#
|
||||
|
@ -1591,11 +1591,15 @@ class PowerOutletFilterSet(
|
||||
queryset=PowerPort.objects.all(),
|
||||
label=_('Power port (ID)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'id', 'name', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
||||
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
||||
)
|
||||
|
||||
|
||||
|
@ -1379,7 +1379,10 @@ class PowerPortBulkEditForm(
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description'])
|
||||
form_from_model(
|
||||
PowerOutlet,
|
||||
['label', 'type', 'status', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description']
|
||||
)
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@ -1389,7 +1392,7 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
FieldSet('module', 'type', 'label', 'description', 'mark_connected', 'color'),
|
||||
FieldSet('module', 'type', 'label', 'status', 'description', 'mark_connected', 'color'),
|
||||
FieldSet('feed_leg', 'power_port', name=_('Power')),
|
||||
)
|
||||
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
|
||||
|
@ -1305,7 +1305,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
@ -1323,6 +1323,11 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=PowerOutletStatusChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
|
@ -1308,7 +1308,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected',
|
||||
'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
|
||||
'description', 'tags',
|
||||
),
|
||||
)
|
||||
@ -1316,7 +1316,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected',
|
||||
'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
|
||||
'description', 'tags',
|
||||
]
|
||||
|
||||
|
16
netbox/dcim/migrations/0201_add_power_outlet_status.py
Normal file
16
netbox/dcim/migrations/0201_add_power_outlet_status.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0200_populate_mac_addresses'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='status',
|
||||
field=models.CharField(default='enabled', max_length=50),
|
||||
),
|
||||
]
|
@ -449,6 +449,12 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
"""
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
choices=PowerOutletStatusChoices,
|
||||
default=PowerOutletStatusChoices.STATUS_ENABLED
|
||||
)
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
@ -492,6 +498,9 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
_("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port)
|
||||
)
|
||||
|
||||
def get_status_color(self):
|
||||
return PowerOutletStatusChoices.colors.get(self.status)
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
|
@ -224,7 +224,7 @@ class PowerOutletIndex(SearchIndex):
|
||||
('label', 200),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('device', 'label', 'type', 'description')
|
||||
display_attrs = ('device', 'label', 'type', 'status', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
@ -520,6 +520,9 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
|
||||
verbose_name=_('Power Port'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:poweroutlet_list'
|
||||
@ -530,9 +533,11 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
|
||||
'color', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items',
|
||||
'tags', 'created', 'last_updated',
|
||||
'tags', 'created', 'last_updated', 'status',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
|
||||
class DevicePowerOutletTable(PowerOutletTable):
|
||||
@ -550,9 +555,11 @@ class DevicePowerOutletTable(PowerOutletTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'power_port', 'feed_leg',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||
'status',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
|
||||
'pk', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'description', 'cable',
|
||||
'connection',
|
||||
)
|
||||
|
||||
|
||||
|
@ -3684,6 +3684,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
|
||||
description='First',
|
||||
color='ff0000',
|
||||
status=PowerOutletStatusChoices.STATUS_ENABLED,
|
||||
),
|
||||
PowerOutlet(
|
||||
device=devices[1],
|
||||
@ -3693,6 +3694,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
description='Second',
|
||||
color='00ff00',
|
||||
status=PowerOutletStatusChoices.STATUS_DISABLED,
|
||||
),
|
||||
PowerOutlet(
|
||||
device=devices[2],
|
||||
@ -3702,6 +3704,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C,
|
||||
description='Third',
|
||||
color='0000ff',
|
||||
status=PowerOutletStatusChoices.STATUS_FAULTY,
|
||||
),
|
||||
)
|
||||
PowerOutlet.objects.bulk_create(power_outlets)
|
||||
@ -3796,6 +3799,23 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [PowerOutletStatusChoices.STATUS_ENABLED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
params = {'status': [PowerOutletStatusChoices.STATUS_DISABLED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
params = {'status': [PowerOutletStatusChoices.STATUS_FAULTY]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
params = {'status': [
|
||||
PowerOutletStatusChoices.STATUS_ENABLED,
|
||||
PowerOutletStatusChoices.STATUS_DISABLED,
|
||||
PowerOutletStatusChoices.STATUS_FAULTY,
|
||||
]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = Interface.objects.all()
|
||||
|
@ -1,6 +1,8 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices, InterfaceModeChoices
|
||||
from dcim.choices import (
|
||||
DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices, InterfaceModeChoices, PowerOutletStatusChoices
|
||||
)
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
@ -12,6 +14,56 @@ def get_id(model, slug):
|
||||
return model.objects.get(slug=slug).id
|
||||
|
||||
|
||||
class PowerOutletFormTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.site = site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
cls.manufacturer = manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
cls.role = role = DeviceRole.objects.create(
|
||||
name='Device Role 1', slug='device-role-1', color='ff0000'
|
||||
)
|
||||
cls.device_type = device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
|
||||
)
|
||||
cls.rack = rack = Rack.objects.create(name='Rack 1', site=site)
|
||||
cls.device = Device.objects.create(
|
||||
name='Device 1', device_type=device_type, role=role, site=site, rack=rack, position=1
|
||||
)
|
||||
|
||||
def test_status_is_required(self):
|
||||
form = PowerOutletForm(data={
|
||||
'device': self.device,
|
||||
'module': None,
|
||||
'name': 'New Enabled Outlet',
|
||||
})
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('status', form.errors)
|
||||
|
||||
def test_status_must_be_defined_choice(self):
|
||||
form = PowerOutletForm(data={
|
||||
'device': self.device,
|
||||
'module': None,
|
||||
'name': 'New Enabled Outlet',
|
||||
'status': 'this isn\'t a defined choice',
|
||||
})
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('status', form.errors)
|
||||
self.assertTrue(form.errors['status'][-1].startswith('Select a valid choice.'))
|
||||
|
||||
def test_status_recognizes_choices(self):
|
||||
for index, choice in enumerate(PowerOutletStatusChoices.CHOICES):
|
||||
form = PowerOutletForm(data={
|
||||
'device': self.device,
|
||||
'module': None,
|
||||
'name': f'New Enabled Outlet {index + 1}',
|
||||
'status': choice[0],
|
||||
})
|
||||
self.assertEqual({}, form.errors)
|
||||
self.assertTrue(form.is_valid())
|
||||
instance = form.save()
|
||||
self.assertEqual(instance.status, choice[0])
|
||||
|
||||
|
||||
class DeviceTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
|
@ -465,7 +465,8 @@ class DeviceTestCase(TestCase):
|
||||
device=device,
|
||||
name='Power Outlet 1',
|
||||
power_port=powerport,
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
|
||||
status=PowerOutletStatusChoices.STATUS_ENABLED,
|
||||
)
|
||||
self.assertEqual(poweroutlet.cf['cf1'], 'foo')
|
||||
|
||||
|
@ -2513,6 +2513,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet X',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'status': PowerOutletStatusChoices.STATUS_ENABLED,
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'A power outlet',
|
||||
@ -2523,6 +2524,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet [4-6]',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'status': PowerOutletStatusChoices.STATUS_ENABLED,
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'A power outlet',
|
||||
@ -2531,6 +2533,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C15,
|
||||
'status': PowerOutletStatusChoices.STATUS_ENABLED,
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'New description',
|
||||
|
@ -36,6 +36,10 @@
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
|
Loading…
Reference in New Issue
Block a user