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

Closes #10371: Add operational status field for modules

This commit is contained in:
jeremystretch
2022-12-09 10:43:29 -05:00
parent b2f34cec19
commit 97aa40f7a8
17 changed files with 123 additions and 29 deletions

View File

@ -58,9 +58,11 @@ The following model fields support configurable choices:
* `circuits.Circuit.status` * `circuits.Circuit.status`
* `dcim.Device.status` * `dcim.Device.status`
* `dcim.Location.status` * `dcim.Location.status`
* `dcim.Module.status`
* `dcim.PowerFeed.status` * `dcim.PowerFeed.status`
* `dcim.Rack.status` * `dcim.Rack.status`
* `dcim.Site.status` * `dcim.Site.status`
* `dcim.VirtualDeviceContext.status`
* `extras.JournalEntry.kind` * `extras.JournalEntry.kind`
* `ipam.IPAddress.status` * `ipam.IPAddress.status`
* `ipam.IPRange.status` * `ipam.IPRange.status`

View File

@ -18,6 +18,13 @@ The [module bay](./modulebay.md) into which the module is installed.
The [module type](./moduletype.md) which represents the physical make & model of hardware. By default, module components will be instantiated automatically from the module type when creating a new module. The [module type](./moduletype.md) which represents the physical make & model of hardware. By default, module components will be instantiated automatically from the module type when creating a new module.
### Status
The module's operational status.
!!! tip
Additional statuses may be defined by setting `Module.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Serial Number ### Serial Number
The unique physical serial number assigned to this module by its manufacturer. The unique physical serial number assigned to this module by its manufacturer.

View File

@ -5,6 +5,7 @@
### Enhancements ### Enhancements
* [#815](https://github.com/netbox-community/netbox/issues/815) - Enable specifying terminations when bulk importing circuits * [#815](https://github.com/netbox-community/netbox/issues/815) - Enable specifying terminations when bulk importing circuits
* [#10371](https://github.com/netbox-community/netbox/issues/10371) - Add operational status field for modules
* [#10945](https://github.com/netbox-community/netbox/issues/10945) - Enabled recurring execution of scheduled reports & scripts * [#10945](https://github.com/netbox-community/netbox/issues/10945) - Enabled recurring execution of scheduled reports & scripts
* [#11090](https://github.com/netbox-community/netbox/issues/11090) - Add regular expression support to global search engine * [#11090](https://github.com/netbox-community/netbox/issues/11090) - Add regular expression support to global search engine
* [#11022](https://github.com/netbox-community/netbox/issues/11022) - Introduce `QUEUE_MAPPINGS` configuration parameter to allow customization of background task prioritization * [#11022](https://github.com/netbox-community/netbox/issues/11022) - Introduce `QUEUE_MAPPINGS` configuration parameter to allow customization of background task prioritization
@ -134,6 +135,8 @@ This release introduces a new programmatic API that enables plugins and custom s
* Added a `description` field * Added a `description` field
* dcim.Interface * dcim.Interface
* Added the `vdcs` field * Added the `vdcs` field
* dcim.Module
* Added a `status` field
* dcim.ModuleType * dcim.ModuleType
* Added a `description` field * Added a `description` field
* Added optional `weight` and `weight_unit` fields * Added optional `weight` and `weight_unit` fields

View File

@ -697,8 +697,8 @@ class ModuleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Module model = Module
fields = [ fields = [
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]

View File

@ -194,6 +194,30 @@ class DeviceAirflowChoices(ChoiceSet):
) )
#
# Modules
#
class ModuleStatusChoices(ChoiceSet):
key = 'Module.status'
STATUS_OFFLINE = 'offline'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_STAGED = 'staged'
STATUS_FAILED = 'failed'
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = [
(STATUS_OFFLINE, 'Offline', 'gray'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGED, 'Staged', 'blue'),
(STATUS_FAILED, 'Failed', 'red'),
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
]
# #
# ConsolePorts # ConsolePorts
# #

View File

@ -1082,13 +1082,17 @@ class ModuleFilterSet(NetBoxModelFilterSet):
queryset=Device.objects.all(), queryset=Device.objects.all(),
label=_('Device (ID)'), label=_('Device (ID)'),
) )
status = django_filters.MultipleChoiceFilter(
choices=ModuleStatusChoices,
null_value=None
)
serial = MultiValueCharFilter( serial = MultiValueCharFilter(
lookup_expr='iexact' lookup_expr='iexact'
) )
class Meta: class Meta:
model = Module model = Module
fields = ['id', 'asset_tag'] fields = ['id', 'status', 'asset_tag']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -574,6 +574,12 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
'manufacturer_id': '$manufacturer' 'manufacturer_id': '$manufacturer'
} }
) )
status = forms.ChoiceField(
choices=add_blank_choice(ModuleStatusChoices),
required=False,
initial='',
widget=StaticSelect()
)
serial = forms.CharField( serial = forms.CharField(
max_length=50, max_length=50,
required=False, required=False,
@ -590,7 +596,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('manufacturer', 'module_type', 'serial', 'description')), (None, ('manufacturer', 'module_type', 'status', 'serial', 'description')),
) )
nullable_fields = ('serial', 'description', 'comments') nullable_fields = ('serial', 'description', 'comments')

View File

@ -450,11 +450,15 @@ class ModuleImportForm(NetBoxModelImportForm):
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
to_field_name='model' to_field_name='model'
) )
status = CSVChoiceField(
choices=ModuleStatusChoices,
help_text=_('Operational status')
)
class Meta: class Meta:
model = Module model = Module
fields = ( fields = (
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'comments', 'tags', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'status', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):

View File

@ -763,7 +763,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')), ('Hardware', ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -780,6 +780,10 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
label=_('Type'), label=_('Type'),
fetch_trigger='open' fetch_trigger='open'
) )
status = MultipleChoiceField(
choices=ModuleStatusChoices,
required=False
)
serial = forms.CharField( serial = forms.CharField(
required=False required=False
) )

View File

@ -703,7 +703,7 @@ class ModuleForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
('Module', ( ('Module', (
'device', 'module_bay', 'manufacturer', 'module_type', 'description', 'tags', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'description', 'tags',
)), )),
('Hardware', ( ('Hardware', (
'serial', 'asset_tag', 'replicate_components', 'adopt_components', 'serial', 'asset_tag', 'replicate_components', 'adopt_components',
@ -713,7 +713,7 @@ class ModuleForm(NetBoxModelForm):
class Meta: class Meta:
model = Module model = Module
fields = [ fields = [
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag', 'tags',
'replicate_components', 'adopt_components', 'description', 'comments', 'replicate_components', 'adopt_components', 'description', 'comments',
] ]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-12-09 15:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0166_virtualdevicecontext'),
]
operations = [
migrations.AddField(
model_name='module',
name='status',
field=models.CharField(default='active', max_length=50),
),
]

View File

@ -925,6 +925,11 @@ class Module(PrimaryModel, ConfigContextModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='instances' related_name='instances'
) )
status = models.CharField(
max_length=50,
choices=ModuleStatusChoices,
default=ModuleStatusChoices.STATUS_ACTIVE
)
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
@ -939,7 +944,7 @@ class Module(PrimaryModel, ConfigContextModel):
help_text=_('A unique tag used to identify this device') help_text=_('A unique tag used to identify this device')
) )
clone_fields = ('device', 'module_type') clone_fields = ('device', 'module_type', 'status')
class Meta: class Meta:
ordering = ('module_bay',) ordering = ('module_bay',)
@ -950,6 +955,9 @@ class Module(PrimaryModel, ConfigContextModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:module', args=[self.pk]) return reverse('dcim:module', args=[self.pk])
def get_status_color(self):
return ModuleStatusChoices.colors.get(self.status)
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -56,6 +56,7 @@ class ModuleTable(NetBoxTable):
module_type = tables.Column( module_type = tables.Column(
linkify=True linkify=True
) )
status = columns.ChoiceFieldColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:module_list' url_name='dcim:module_list'
@ -64,9 +65,9 @@ class ModuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Module model = Module
fields = ( fields = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'description', 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',
'comments', 'tags', 'description', 'comments', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',
) )

View File

@ -1271,6 +1271,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
'device': device.pk, 'device': device.pk,
'module_bay': module_bays[3].pk, 'module_bay': module_bays[3].pk,
'module_type': module_types[0].pk, 'module_type': module_types[0].pk,
'status': ModuleStatusChoices.STATUS_ACTIVE,
'serial': 'ABC123', 'serial': 'ABC123',
'asset_tag': 'Foo1', 'asset_tag': 'Foo1',
}, },
@ -1278,6 +1279,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
'device': device.pk, 'device': device.pk,
'module_bay': module_bays[4].pk, 'module_bay': module_bays[4].pk,
'module_type': module_types[1].pk, 'module_type': module_types[1].pk,
'status': ModuleStatusChoices.STATUS_ACTIVE,
'serial': 'DEF456', 'serial': 'DEF456',
'asset_tag': 'Foo2', 'asset_tag': 'Foo2',
}, },
@ -1285,6 +1287,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
'device': device.pk, 'device': device.pk,
'module_bay': module_bays[5].pk, 'module_bay': module_bays[5].pk,
'module_type': module_types[2].pk, 'module_type': module_types[2].pk,
'status': ModuleStatusChoices.STATUS_ACTIVE,
'serial': 'GHI789', 'serial': 'GHI789',
'asset_tag': 'Foo3', 'asset_tag': 'Foo3',
}, },

View File

@ -1876,15 +1876,15 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
ModuleBay.objects.bulk_create(module_bays) ModuleBay.objects.bulk_create(module_bays)
modules = ( modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0], serial='A', asset_tag='A'), Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0], status=ModuleStatusChoices.STATUS_ACTIVE, serial='A', asset_tag='A'),
Module(device=devices[0], module_bay=module_bays[1], module_type=module_types[1], serial='B', asset_tag='B'), Module(device=devices[0], module_bay=module_bays[1], module_type=module_types[1], status=ModuleStatusChoices.STATUS_ACTIVE, serial='B', asset_tag='B'),
Module(device=devices[0], module_bay=module_bays[2], module_type=module_types[2], serial='C', asset_tag='C'), Module(device=devices[0], module_bay=module_bays[2], module_type=module_types[2], status=ModuleStatusChoices.STATUS_ACTIVE, serial='C', asset_tag='C'),
Module(device=devices[1], module_bay=module_bays[3], module_type=module_types[0], serial='D', asset_tag='D'), Module(device=devices[1], module_bay=module_bays[3], module_type=module_types[0], status=ModuleStatusChoices.STATUS_ACTIVE, serial='D', asset_tag='D'),
Module(device=devices[1], module_bay=module_bays[4], module_type=module_types[1], serial='E', asset_tag='E'), Module(device=devices[1], module_bay=module_bays[4], module_type=module_types[1], status=ModuleStatusChoices.STATUS_ACTIVE, serial='E', asset_tag='E'),
Module(device=devices[1], module_bay=module_bays[5], module_type=module_types[2], serial='F', asset_tag='F'), Module(device=devices[1], module_bay=module_bays[5], module_type=module_types[2], status=ModuleStatusChoices.STATUS_ACTIVE, serial='F', asset_tag='F'),
Module(device=devices[2], module_bay=module_bays[6], module_type=module_types[0], serial='G', asset_tag='G'), Module(device=devices[2], module_bay=module_bays[6], module_type=module_types[0], status=ModuleStatusChoices.STATUS_ACTIVE, serial='G', asset_tag='G'),
Module(device=devices[2], module_bay=module_bays[7], module_type=module_types[1], serial='H', asset_tag='H'), Module(device=devices[2], module_bay=module_bays[7], module_type=module_types[1], status=ModuleStatusChoices.STATUS_PLANNED, serial='H', asset_tag='H'),
Module(device=devices[2], module_bay=module_bays[8], module_type=module_types[2], serial='I', asset_tag='I'), Module(device=devices[2], module_bay=module_bays[8], module_type=module_types[2], status=ModuleStatusChoices.STATUS_FAILED, serial='I', asset_tag='I'),
) )
Module.objects.bulk_create(modules) Module.objects.bulk_create(modules)
@ -1912,6 +1912,10 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_id': [device_types[0].pk, device_types[1].pk]} params = {'device_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_status(self):
params = {'status': [ModuleStatusChoices.STATUS_PLANNED, ModuleStatusChoices.STATUS_FAILED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_serial(self): def test_serial(self):
params = {'serial': ['A', 'B']} params = {'serial': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -1887,26 +1887,28 @@ class ModuleTestCase(
'device': devices[0].pk, 'device': devices[0].pk,
'module_bay': module_bays[3].pk, 'module_bay': module_bays[3].pk,
'module_type': module_types[0].pk, 'module_type': module_types[0].pk,
'status': ModuleStatusChoices.STATUS_ACTIVE,
'serial': 'A', 'serial': 'A',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
cls.bulk_edit_data = { cls.bulk_edit_data = {
'module_type': module_types[3].pk, 'module_type': module_types[3].pk,
'status': ModuleStatusChoices.STATUS_PLANNED,
} }
cls.csv_data = ( cls.csv_data = (
"device,module_bay,module_type,serial,asset_tag", "device,module_bay,module_type,status,serial,asset_tag",
"Device 2,Module Bay 1,Module Type 1,A,A", "Device 2,Module Bay 1,Module Type 1,active,A,A",
"Device 2,Module Bay 2,Module Type 2,B,B", "Device 2,Module Bay 2,Module Type 2,planned,B,B",
"Device 2,Module Bay 3,Module Type 3,C,C", "Device 2,Module Bay 3,Module Type 3,failed,C,C",
) )
cls.csv_update_data = ( cls.csv_update_data = (
"id,serial", "id,status,serial",
f"{modules[0].pk},Serial 2", f"{modules[0].pk},offline,Serial 2",
f"{modules[1].pk},Serial 3", f"{modules[1].pk},offline,Serial 3",
f"{modules[2].pk},Serial 1", f"{modules[2].pk},offline,Serial 1",
) )
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])

View File

@ -62,6 +62,10 @@
<th scope="row">Module Type</th> <th scope="row">Module Type</th>
<td>{{ object.module_type|linkify }}</td> <td>{{ object.module_type|linkify }}</td>
</tr> </tr>
<tr>
<th scope="row">Status</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>