diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5b9c26876..795040b17 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -325,7 +325,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): site = NestedSiteSerializer() rack = NestedRackSerializer(required=False, allow_null=True) face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) - status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) + status = ChoiceField(choices=DeviceStatusChoices, required=False) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index ac161daf8..2ba3845f6 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -129,6 +129,37 @@ class DeviceFaceChoices(ChoiceSet): } +class DeviceStatusChoices(ChoiceSet): + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_STAGED = 'staged' + STATUS_FAILED = 'failed' + STATUS_INVENTORY = 'inventory' + STATUS_DECOMMISSIONING = 'decommissioning' + + CHOICES = ( + (STATUS_OFFLINE, 'Offline'), + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_STAGED, 'Staged'), + (STATUS_FAILED, 'Failed'), + (STATUS_INVENTORY, 'Inventory'), + (STATUS_DECOMMISSIONING, 'Decommissioning'), + ) + + LEGACY_MAP = { + STATUS_OFFLINE: 0, + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_STAGED: 3, + STATUS_FAILED: 4, + STATUS_INVENTORY: 5, + STATUS_DECOMMISSIONING: 6, + } + + # # ConsolePorts # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 71fbd6cd2..a1b6744b7 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -61,24 +61,6 @@ PORT_TYPE_CHOICES = [ ] ] -# Device statuses -DEVICE_STATUS_OFFLINE = 0 -DEVICE_STATUS_ACTIVE = 1 -DEVICE_STATUS_PLANNED = 2 -DEVICE_STATUS_STAGED = 3 -DEVICE_STATUS_FAILED = 4 -DEVICE_STATUS_INVENTORY = 5 -DEVICE_STATUS_DECOMMISSIONING = 6 -DEVICE_STATUS_CHOICES = [ - [DEVICE_STATUS_ACTIVE, 'Active'], - [DEVICE_STATUS_OFFLINE, 'Offline'], - [DEVICE_STATUS_PLANNED, 'Planned'], - [DEVICE_STATUS_STAGED, 'Staged'], - [DEVICE_STATUS_FAILED, 'Failed'], - [DEVICE_STATUS_INVENTORY, 'Inventory'], - [DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'], -] - # Bootstrap CSS classes for device/rack statuses STATUS_CLASSES = { 0: 'warning', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 4620ea17a..e3c6f64ac 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -511,7 +511,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter label='Device model (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, null_value=None ) is_full_depth = django_filters.BooleanFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 15dc79ec5..d6c852c2d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1665,7 +1665,7 @@ class BaseDeviceCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, help_text='Operational status' ) @@ -1833,7 +1833,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ) ) status = forms.ChoiceField( - choices=add_blank_choice(DEVICE_STATUS_CHOICES), + choices=add_blank_choice(DeviceStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -1944,7 +1944,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt ) ) status = forms.MultipleChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/dcim/migrations/0081_3569_device_fields.py b/netbox/dcim/migrations/0081_3569_device_fields.py index fc1e9285a..f1f0bdb2b 100644 --- a/netbox/dcim/migrations/0081_3569_device_fields.py +++ b/netbox/dcim/migrations/0081_3569_device_fields.py @@ -5,6 +5,16 @@ DEVICE_FACE_CHOICES = ( (1, 'rear'), ) +DEVICE_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (3, 'staged'), + (4, 'failed'), + (5, 'inventory'), + (6, 'decommissioning'), +) + def device_face_to_slug(apps, schema_editor): Device = apps.get_model('dcim', 'Device') @@ -12,6 +22,12 @@ def device_face_to_slug(apps, schema_editor): Device.objects.filter(face=str(id)).update(face=slug) +def device_status_to_slug(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for id, slug in DEVICE_STATUS_CHOICES: + Device.objects.filter(status=str(id)).update(status=slug) + + class Migration(migrations.Migration): atomic = False @@ -20,6 +36,8 @@ class Migration(migrations.Migration): ] operations = [ + + # Device.face migrations.AlterField( model_name='device', name='face', @@ -33,4 +51,15 @@ class Migration(migrations.Migration): name='face', field=models.CharField(blank=True, max_length=50), ), + + # Device.status + migrations.AlterField( + model_name='device', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=device_status_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c8d9c8250..f9707a90a 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1551,10 +1551,10 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): choices=DeviceFaceChoices, verbose_name='Rack face' ) - status = models.PositiveSmallIntegerField( - choices=DEVICE_STATUS_CHOICES, - default=DEVICE_STATUS_ACTIVE, - verbose_name='Status' + status = models.CharField( + max_length=50, + choices=DeviceStatusChoices, + default=DeviceStatusChoices.STATUS_ACTIVE ) primary_ip4 = models.OneToOneField( to='ipam.IPAddress', @@ -1616,6 +1616,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] + STATUS_CLASS_MAP = { + DeviceStatusChoices.STATUS_OFFLINE: 'warning', + DeviceStatusChoices.STATUS_ACTIVE: 'success', + DeviceStatusChoices.STATUS_PLANNED: 'info', + DeviceStatusChoices.STATUS_STAGED: 'primary', + DeviceStatusChoices.STATUS_FAILED: 'danger', + DeviceStatusChoices.STATUS_INVENTORY: 'default', + DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning', + } + class Meta: ordering = ['name'] unique_together = [ @@ -1869,7 +1879,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): return Device.objects.filter(parent_bay__device=self.pk) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) # diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 8ac9aa84e..d7a946568 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -24,7 +24,7 @@ class DeviceTestCase(TestCase): 'face': DeviceFaceChoices.FACE_FRONT, 'position': 41, 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid(), test.fields['position'].choices) self.assertTrue(test.save()) @@ -41,7 +41,7 @@ class DeviceTestCase(TestCase): 'face': DeviceFaceChoices.FACE_FRONT, 'position': 1, 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertFalse(test.is_valid()) @@ -57,7 +57,7 @@ class DeviceTestCase(TestCase): 'face': '', 'position': None, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid()) self.assertTrue(test.save()) @@ -74,7 +74,7 @@ class DeviceTestCase(TestCase): 'face': DeviceFaceChoices.FACE_REAR, 'position': None, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid()) self.assertTrue(test.save()) diff --git a/netbox/virtualization/constants.py b/netbox/virtualization/constants.py index 37e9efea2..3eeddd066 100644 --- a/netbox/virtualization/constants.py +++ b/netbox/virtualization/constants.py @@ -1,10 +1,10 @@ -from dcim.constants import DEVICE_STATUS_ACTIVE, DEVICE_STATUS_OFFLINE, DEVICE_STATUS_STAGED +from dcim.choices import DeviceStatusChoices # VirtualMachine statuses (replicated from Device statuses) VM_STATUS_CHOICES = [ - [DEVICE_STATUS_ACTIVE, 'Active'], - [DEVICE_STATUS_OFFLINE, 'Offline'], - [DEVICE_STATUS_STAGED, 'Staged'], + [1, 'Active'], + [0, 'Offline'], + [3, 'Staged'], ] # Bootstrap CSS classes for VirtualMachine statuses diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index c47f516cf..790c9c190 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -195,7 +195,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): ) status = models.PositiveSmallIntegerField( choices=VM_STATUS_CHOICES, - default=DEVICE_STATUS_ACTIVE, + default=1, # TODO: Replace with ChoiceSet value verbose_name='Status' ) role = models.ForeignKey(