Browse Source

Merge pull request #3732 from netbox-community/3569-api-choice-slugs

Replace API integer API choice values with slugs
Jeremy Stretch 6 years ago
parent
commit
dcb2c4722c
76 changed files with 2760 additions and 1401 deletions
  1. 2 2
      netbox/circuits/api/serializers.py
  2. 48 0
      netbox/circuits/choices.py
  3. 0 23
      netbox/circuits/constants.py
  4. 2 2
      netbox/circuits/filters.py
  5. 4 4
      netbox/circuits/forms.py
  6. 39 0
      netbox/circuits/migrations/0016_3569_circuit_fields.py
  7. 17 7
      netbox/circuits/models.py
  8. 28 13
      netbox/circuits/tests/test_api.py
  9. 9 5
      netbox/circuits/views.py
  10. 34 34
      netbox/dcim/api/serializers.py
  11. 493 120
      netbox/dcim/choices.py
  12. 9 469
      netbox/dcim/constants.py
  13. 9 9
      netbox/dcim/filters.py
  14. 11 11
      netbox/dcim/fixtures/dcim.json
  15. 82 99
      netbox/dcim/forms.py
  16. 35 0
      netbox/dcim/migrations/0078_3569_site_fields.py
  17. 92 0
      netbox/dcim/migrations/0079_3569_rack_fields.py
  18. 39 0
      netbox/dcim/migrations/0080_3569_devicetype_fields.py
  19. 65 0
      netbox/dcim/migrations/0081_3569_device_fields.py
  20. 147 0
      netbox/dcim/migrations/0082_3569_interface_fields.py
  21. 93 0
      netbox/dcim/migrations/0082_3569_port_fields.py
  22. 85 0
      netbox/dcim/migrations/0083_3569_cable_fields.py
  23. 100 0
      netbox/dcim/migrations/0084_3569_powerfeed_fields.py
  24. 62 0
      netbox/dcim/migrations/0085_3569_poweroutlet_fields.py
  25. 151 102
      netbox/dcim/models.py
  26. 0 8
      netbox/dcim/tables.py
  27. 44 43
      netbox/dcim/tests/test_api.py
  28. 8 8
      netbox/dcim/tests/test_forms.py
  29. 17 17
      netbox/dcim/tests/test_models.py
  30. 19 19
      netbox/dcim/tests/test_views.py
  31. 6 6
      netbox/extras/api/customfields.py
  32. 4 3
      netbox/extras/api/serializers.py
  33. 140 0
      netbox/extras/choices.py
  34. 0 87
      netbox/extras/constants.py
  35. 9 3
      netbox/extras/filters.py
  36. 8 7
      netbox/extras/forms.py
  37. 7 7
      netbox/extras/middleware.py
  38. 0 2
      netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py
  39. 5 7
      netbox/extras/migrations/0010_customfield_filter_logic.py
  40. 69 0
      netbox/extras/migrations/0029_3569_customfield_fields.py
  41. 36 0
      netbox/extras/migrations/0030_3569_objectchange_fields.py
  42. 35 0
      netbox/extras/migrations/0031_3569_exporttemplate_fields.py
  43. 35 0
      netbox/extras/migrations/0032_3569_webhook_fields.py
  44. 35 26
      netbox/extras/models.py
  45. 3 3
      netbox/extras/tables.py
  46. 5 4
      netbox/extras/tests/test_changelog.py
  47. 17 17
      netbox/extras/tests/test_customfields.py
  48. 2 2
      netbox/extras/tests/test_views.py
  49. 4 3
      netbox/extras/webhooks.py
  50. 2 1
      netbox/extras/webhooks_worker.py
  51. 7 7
      netbox/ipam/api/serializers.py
  52. 130 0
      netbox/ipam/choices.py
  53. 0 97
      netbox/ipam/constants.py
  54. 5 5
      netbox/ipam/filters.py
  55. 3 3
      netbox/ipam/fixtures/ipam.json
  56. 15 15
      netbox/ipam/forms.py
  57. 37 0
      netbox/ipam/migrations/0028_3569_prefix_fields.py
  58. 69 0
      netbox/ipam/migrations/0029_3569_ipaddress_fields.py
  59. 36 0
      netbox/ipam/migrations/0030_3569_vlan_fields.py
  60. 35 0
      netbox/ipam/migrations/0031_3569_service_fields.py
  61. 74 25
      netbox/ipam/models.py
  62. 9 9
      netbox/ipam/tests/test_api.py
  63. 3 3
      netbox/ipam/tests/test_models.py
  64. 4 4
      netbox/ipam/tests/test_views.py
  65. 6 6
      netbox/ipam/views.py
  66. 14 1
      netbox/utilities/api.py
  67. 36 0
      netbox/utilities/choices.py
  68. 12 6
      netbox/utilities/utils.py
  69. 5 5
      netbox/virtualization/api/serializers.py
  70. 24 0
      netbox/virtualization/choices.py
  71. 0 15
      netbox/virtualization/constants.py
  72. 2 2
      netbox/virtualization/filters.py
  73. 12 12
      netbox/virtualization/forms.py
  74. 36 0
      netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py
  75. 12 5
      netbox/virtualization/models.py
  76. 8 8
      netbox/virtualization/tests/test_api.py

+ 2 - 2
netbox/circuits/api/serializers.py

@@ -1,7 +1,7 @@
 from rest_framework import serializers
 from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 
-from circuits.constants import CIRCUIT_STATUS_CHOICES
+from circuits.choices import CircuitStatusChoices
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.serializers import ConnectedEndpointSerializer
 from dcim.api.serializers import ConnectedEndpointSerializer
@@ -41,7 +41,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
 
 
 class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
     provider = NestedProviderSerializer()
     provider = NestedProviderSerializer()
-    status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=CircuitStatusChoices, required=False)
     type = NestedCircuitTypeSerializer()
     type = NestedCircuitTypeSerializer()
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)

+ 48 - 0
netbox/circuits/choices.py

@@ -0,0 +1,48 @@
+from utilities.choices import ChoiceSet
+
+
+#
+# Circuits
+#
+
+class CircuitStatusChoices(ChoiceSet):
+
+    STATUS_DEPROVISIONING = 'deprovisioning'
+    STATUS_ACTIVE = 'active'
+    STATUS_PLANNED = 'planned'
+    STATUS_PROVISIONING = 'provisioning'
+    STATUS_OFFLINE = 'offline'
+    STATUS_DECOMMISSIONED = 'decommissioned'
+
+    CHOICES = (
+        (STATUS_PLANNED, 'Planned'),
+        (STATUS_PROVISIONING, 'Provisioning'),
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_OFFLINE, 'Offline'),
+        (STATUS_DEPROVISIONING, 'Deprovisioning'),
+        (STATUS_DECOMMISSIONED, 'Decommissioned'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_DEPROVISIONING: 0,
+        STATUS_ACTIVE: 1,
+        STATUS_PLANNED: 2,
+        STATUS_PROVISIONING: 3,
+        STATUS_OFFLINE: 4,
+        STATUS_DECOMMISSIONED: 5,
+    }
+
+
+#
+# CircuitTerminations
+#
+
+class CircuitTerminationSideChoices(ChoiceSet):
+
+    SIDE_A = 'A'
+    SIDE_Z = 'Z'
+
+    CHOICES = (
+        (SIDE_A, 'A'),
+        (SIDE_Z, 'Z')
+    )

+ 0 - 23
netbox/circuits/constants.py

@@ -1,23 +0,0 @@
-# Circuit statuses
-CIRCUIT_STATUS_DEPROVISIONING = 0
-CIRCUIT_STATUS_ACTIVE = 1
-CIRCUIT_STATUS_PLANNED = 2
-CIRCUIT_STATUS_PROVISIONING = 3
-CIRCUIT_STATUS_OFFLINE = 4
-CIRCUIT_STATUS_DECOMMISSIONED = 5
-CIRCUIT_STATUS_CHOICES = [
-    [CIRCUIT_STATUS_PLANNED, 'Planned'],
-    [CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
-    [CIRCUIT_STATUS_ACTIVE, 'Active'],
-    [CIRCUIT_STATUS_OFFLINE, 'Offline'],
-    [CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
-    [CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
-]
-
-# CircuitTermination sides
-TERM_SIDE_A = 'A'
-TERM_SIDE_Z = 'Z'
-TERM_SIDE_CHOICES = (
-    (TERM_SIDE_A, 'A'),
-    (TERM_SIDE_Z, 'Z'),
-)

+ 2 - 2
netbox/circuits/filters.py

@@ -5,7 +5,7 @@ from dcim.models import Region, Site
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
-from .constants import *
+from .choices import *
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 
@@ -84,7 +84,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
         label='Circuit type (slug)',
         label='Circuit type (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=CIRCUIT_STATUS_CHOICES,
+        choices=CircuitStatusChoices,
         null_value=None
         null_value=None
     )
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(

+ 4 - 4
netbox/circuits/forms.py

@@ -9,7 +9,7 @@ from utilities.forms import (
     APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
     APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
     FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
     FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
 )
 )
-from .constants import *
+from .choices import CircuitStatusChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 
@@ -194,7 +194,7 @@ class CircuitCSVForm(forms.ModelForm):
         }
         }
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=CIRCUIT_STATUS_CHOICES,
+        choices=CircuitStatusChoices,
         required=False,
         required=False,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
@@ -235,7 +235,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
+        choices=add_blank_choice(CircuitStatusChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -292,7 +292,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=CIRCUIT_STATUS_CHOICES,
+        choices=CircuitStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )

+ 39 - 0
netbox/circuits/migrations/0016_3569_circuit_fields.py

@@ -0,0 +1,39 @@
+from django.db import migrations, models
+
+
+CIRCUIT_STATUS_CHOICES = (
+    (0, 'deprovisioning'),
+    (1, 'active'),
+    (2, 'planned'),
+    (3, 'provisioning'),
+    (4, 'offline'),
+    (5, 'decommissioned')
+)
+
+
+def circuit_status_to_slug(apps, schema_editor):
+    Circuit = apps.get_model('circuits', 'Circuit')
+    for id, slug in CIRCUIT_STATUS_CHOICES:
+        Circuit.objects.filter(status=str(id)).update(status=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('circuits', '0015_custom_tag_models'),
+    ]
+
+    operations = [
+
+        # Circuit.status
+        migrations.AlterField(
+            model_name='circuit',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=circuit_status_to_slug
+        ),
+
+    ]

+ 17 - 7
netbox/circuits/models.py

@@ -3,13 +3,13 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
-from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
+from dcim.constants import CONNECTION_STATUS_CHOICES
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from dcim.models import CableTermination
 from dcim.models import CableTermination
 from extras.models import CustomFieldModel, ObjectChange, TaggedItem
 from extras.models import CustomFieldModel, ObjectChange, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 from utilities.utils import serialize_object
 from utilities.utils import serialize_object
-from .constants import *
+from .choices import *
 
 
 
 
 class Provider(ChangeLoggedModel, CustomFieldModel):
 class Provider(ChangeLoggedModel, CustomFieldModel):
@@ -132,9 +132,10 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
         related_name='circuits'
         related_name='circuits'
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=CIRCUIT_STATUS_CHOICES,
-        default=CIRCUIT_STATUS_ACTIVE
+    status = models.CharField(
+        max_length=50,
+        choices=CircuitStatusChoices,
+        default=CircuitStatusChoices.STATUS_ACTIVE
     )
     )
     tenant = models.ForeignKey(
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         to='tenancy.Tenant',
@@ -171,6 +172,15 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',
+        CircuitStatusChoices.STATUS_ACTIVE: 'success',
+        CircuitStatusChoices.STATUS_PLANNED: 'info',
+        CircuitStatusChoices.STATUS_PROVISIONING: 'primary',
+        CircuitStatusChoices.STATUS_OFFLINE: 'danger',
+        CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default',
+    }
+
     class Meta:
     class Meta:
         ordering = ['provider', 'cid']
         ordering = ['provider', 'cid']
         unique_together = ['provider', 'cid']
         unique_together = ['provider', 'cid']
@@ -195,7 +205,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
         )
         )
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
     def _get_termination(self, side):
     def _get_termination(self, side):
         for ct in self.terminations.all():
         for ct in self.terminations.all():
@@ -220,7 +230,7 @@ class CircuitTermination(CableTermination):
     )
     )
     term_side = models.CharField(
     term_side = models.CharField(
         max_length=1,
         max_length=1,
-        choices=TERM_SIDE_CHOICES,
+        choices=CircuitTerminationSideChoices,
         verbose_name='Termination'
         verbose_name='Termination'
     )
     )
     site = models.ForeignKey(
     site = models.ForeignKey(

+ 28 - 13
netbox/circuits/tests/test_api.py

@@ -1,9 +1,9 @@
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
-from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
+from circuits.choices import *
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
-from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
+from dcim.models import Site
 from extras.constants import GRAPH_TYPE_PROVIDER
 from extras.constants import GRAPH_TYPE_PROVIDER
 from extras.models import Graph
 from extras.models import Graph
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
@@ -250,7 +250,7 @@ class CircuitTest(APITestCase):
             'cid': 'TEST0004',
             'cid': 'TEST0004',
             'provider': self.provider1.pk,
             'provider': self.provider1.pk,
             'type': self.circuittype1.pk,
             'type': self.circuittype1.pk,
-            'status': CIRCUIT_STATUS_ACTIVE,
+            'status': CircuitStatusChoices.STATUS_ACTIVE,
         }
         }
 
 
         url = reverse('circuits-api:circuit-list')
         url = reverse('circuits-api:circuit-list')
@@ -270,19 +270,19 @@ class CircuitTest(APITestCase):
                 'cid': 'TEST0004',
                 'cid': 'TEST0004',
                 'provider': self.provider1.pk,
                 'provider': self.provider1.pk,
                 'type': self.circuittype1.pk,
                 'type': self.circuittype1.pk,
-                'status': CIRCUIT_STATUS_ACTIVE,
+                'status': CircuitStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'cid': 'TEST0005',
                 'cid': 'TEST0005',
                 'provider': self.provider1.pk,
                 'provider': self.provider1.pk,
                 'type': self.circuittype1.pk,
                 'type': self.circuittype1.pk,
-                'status': CIRCUIT_STATUS_ACTIVE,
+                'status': CircuitStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'cid': 'TEST0006',
                 'cid': 'TEST0006',
                 'provider': self.provider1.pk,
                 'provider': self.provider1.pk,
                 'type': self.circuittype1.pk,
                 'type': self.circuittype1.pk,
-                'status': CIRCUIT_STATUS_ACTIVE,
+                'status': CircuitStatusChoices.STATUS_ACTIVE,
             },
             },
         ]
         ]
 
 
@@ -336,16 +336,28 @@ class CircuitTerminationTest(APITestCase):
         self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
         self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
         self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
         self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
         self.circuittermination1 = CircuitTermination.objects.create(
         self.circuittermination1 = CircuitTermination.objects.create(
-            circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
+            circuit=self.circuit1,
+            term_side=CircuitTerminationSideChoices.SIDE_A,
+            site=self.site1,
+            port_speed=1000000
         )
         )
         self.circuittermination2 = CircuitTermination.objects.create(
         self.circuittermination2 = CircuitTermination.objects.create(
-            circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
+            circuit=self.circuit1,
+            term_side=CircuitTerminationSideChoices.SIDE_Z,
+            site=self.site2,
+            port_speed=1000000
         )
         )
         self.circuittermination3 = CircuitTermination.objects.create(
         self.circuittermination3 = CircuitTermination.objects.create(
-            circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
+            circuit=self.circuit2,
+            term_side=CircuitTerminationSideChoices.SIDE_A,
+            site=self.site1,
+            port_speed=1000000
         )
         )
         self.circuittermination4 = CircuitTermination.objects.create(
         self.circuittermination4 = CircuitTermination.objects.create(
-            circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
+            circuit=self.circuit2,
+            term_side=CircuitTerminationSideChoices.SIDE_Z,
+            site=self.site2,
+            port_speed=1000000
         )
         )
 
 
     def test_get_circuittermination(self):
     def test_get_circuittermination(self):
@@ -366,7 +378,7 @@ class CircuitTerminationTest(APITestCase):
 
 
         data = {
         data = {
             'circuit': self.circuit3.pk,
             'circuit': self.circuit3.pk,
-            'term_side': TERM_SIDE_A,
+            'term_side': CircuitTerminationSideChoices.SIDE_A,
             'site': self.site1.pk,
             'site': self.site1.pk,
             'port_speed': 1000000,
             'port_speed': 1000000,
         }
         }
@@ -385,12 +397,15 @@ class CircuitTerminationTest(APITestCase):
     def test_update_circuittermination(self):
     def test_update_circuittermination(self):
 
 
         circuittermination5 = CircuitTermination.objects.create(
         circuittermination5 = CircuitTermination.objects.create(
-            circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
+            circuit=self.circuit3,
+            term_side=CircuitTerminationSideChoices.SIDE_A,
+            site=self.site1,
+            port_speed=1000000
         )
         )
 
 
         data = {
         data = {
             'circuit': self.circuit3.pk,
             'circuit': self.circuit3.pk,
-            'term_side': TERM_SIDE_Z,
+            'term_side': CircuitTerminationSideChoices.SIDE_Z,
             'site': self.site2.pk,
             'site': self.site2.pk,
             'port_speed': 1000000,
             'port_speed': 1000000,
         }
         }

+ 9 - 5
netbox/circuits/views.py

@@ -12,7 +12,7 @@ from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
-from .constants import TERM_SIDE_A, TERM_SIDE_Z
+from .choices import CircuitTerminationSideChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 
@@ -151,12 +151,12 @@ class CircuitView(PermissionRequiredMixin, View):
         termination_a = CircuitTermination.objects.prefetch_related(
         termination_a = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
             'site__region', 'connected_endpoint__device'
         ).filter(
         ).filter(
-            circuit=circuit, term_side=TERM_SIDE_A
+            circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
         ).first()
         ).first()
         termination_z = CircuitTermination.objects.prefetch_related(
         termination_z = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
             'site__region', 'connected_endpoint__device'
         ).filter(
         ).filter(
-            circuit=circuit, term_side=TERM_SIDE_Z
+            circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
         ).first()
         ).first()
 
 
         return render(request, 'circuits/circuit.html', {
         return render(request, 'circuits/circuit.html', {
@@ -212,8 +212,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 def circuit_terminations_swap(request, pk):
 def circuit_terminations_swap(request, pk):
 
 
     circuit = get_object_or_404(Circuit, pk=pk)
     circuit = get_object_or_404(Circuit, pk=pk)
-    termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
-    termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
+    termination_a = CircuitTermination.objects.filter(
+        circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
+    ).first()
+    termination_z = CircuitTermination.objects.filter(
+        circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
+    ).first()
     if not termination_a and not termination_z:
     if not termination_a and not termination_z:
         messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
         messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
         return redirect('circuits:circuit', pk=circuit.pk)
         return redirect('circuits:circuit', pk=circuit.pk)

+ 34 - 34
netbox/dcim/api/serializers.py

@@ -68,7 +68,7 @@ class RegionSerializer(serializers.ModelSerializer):
 
 
 
 
 class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
-    status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=SiteStatusChoices, required=False)
     region = NestedRegionSerializer(required=False, allow_null=True)
     region = NestedRegionSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     time_zone = TimeZoneField(required=False)
     time_zone = TimeZoneField(required=False)
@@ -115,11 +115,11 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
     group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=RackStatusChoices, required=False)
     role = NestedRackRoleSerializer(required=False, allow_null=True)
     role = NestedRackRoleSerializer(required=False, allow_null=True)
-    type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
-    width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
-    outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
+    type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True)
+    width = ChoiceField(choices=RackWidthChoices, required=False)
+    outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
     device_count = serializers.IntegerField(read_only=True)
     device_count = serializers.IntegerField(read_only=True)
     powerfeed_count = serializers.IntegerField(read_only=True)
     powerfeed_count = serializers.IntegerField(read_only=True)
@@ -187,7 +187,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
 
 
 class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
     manufacturer = NestedManufacturerSerializer()
     manufacturer = NestedManufacturerSerializer()
-    subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
+    subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
     device_count = serializers.IntegerField(read_only=True)
     device_count = serializers.IntegerField(read_only=True)
 
 
@@ -202,7 +202,7 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class ConsolePortTemplateSerializer(ValidatedModelSerializer):
 class ConsolePortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         required=False
         required=False
     )
     )
 
 
@@ -214,7 +214,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
 class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
 class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         required=False
         required=False
     )
     )
 
 
@@ -226,7 +226,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
 class PowerPortTemplateSerializer(ValidatedModelSerializer):
 class PowerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=PowerPortTypes.CHOICES,
+        choices=PowerPortTypeChoices,
         required=False
         required=False
     )
     )
 
 
@@ -238,14 +238,14 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
 class PowerOutletTemplateSerializer(ValidatedModelSerializer):
 class PowerOutletTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=PowerOutletTypes.CHOICES,
+        choices=PowerOutletTypeChoices,
         required=False
         required=False
     )
     )
     power_port = PowerPortTemplateSerializer(
     power_port = PowerPortTemplateSerializer(
         required=False
         required=False
     )
     )
     feed_leg = ChoiceField(
     feed_leg = ChoiceField(
-        choices=POWERFEED_LEG_CHOICES,
+        choices=PowerOutletFeedLegChoices,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
@@ -257,7 +257,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
 
 
 class InterfaceTemplateSerializer(ValidatedModelSerializer):
 class InterfaceTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
-    type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
+    type = ChoiceField(choices=InterfaceTypeChoices, required=False)
 
 
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
@@ -266,7 +266,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
 
 
 class RearPortTemplateSerializer(ValidatedModelSerializer):
 class RearPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
 
 
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
@@ -275,7 +275,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
 
 
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
     rear_port = NestedRearPortTemplateSerializer()
     rear_port = NestedRearPortTemplateSerializer()
 
 
     class Meta:
     class Meta:
@@ -324,8 +324,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
     platform = NestedPlatformSerializer(required=False, allow_null=True)
     platform = NestedPlatformSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     rack = NestedRackSerializer(required=False, allow_null=True)
     rack = NestedRackSerializer(required=False, allow_null=True)
-    face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
-    status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
+    face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True)
+    status = ChoiceField(choices=DeviceStatusChoices, required=False)
     primary_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
@@ -388,7 +388,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
 class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         required=False
         required=False
     )
     )
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
@@ -405,7 +405,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
 class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         required=False
         required=False
     )
     )
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
@@ -422,14 +422,14 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=PowerOutletTypes.CHOICES,
+        choices=PowerOutletTypeChoices,
         required=False
         required=False
     )
     )
     power_port = NestedPowerPortSerializer(
     power_port = NestedPowerPortSerializer(
         required=False
         required=False
     )
     )
     feed_leg = ChoiceField(
     feed_leg = ChoiceField(
-        choices=POWERFEED_LEG_CHOICES,
+        choices=PowerOutletFeedLegChoices,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
@@ -451,7 +451,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
-        choices=PowerPortTypes.CHOICES,
+        choices=PowerPortTypeChoices,
         required=False
         required=False
     )
     )
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
@@ -467,9 +467,9 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 
 
 class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
+    type = ChoiceField(choices=InterfaceTypeChoices, required=False)
     lag = NestedInterfaceSerializer(required=False, allow_null=True)
     lag = NestedInterfaceSerializer(required=False, allow_null=True)
-    mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
+    mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     tagged_vlans = SerializedPKRelatedField(
     tagged_vlans = SerializedPKRelatedField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
@@ -511,7 +511,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 
 
 class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
 class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
@@ -533,7 +533,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
 
 
 class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
 class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
     rear_port = FrontPortRearPortSerializer()
     rear_port = FrontPortRearPortSerializer()
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
@@ -586,7 +586,7 @@ class CableSerializer(ValidatedModelSerializer):
     termination_a = serializers.SerializerMethodField(read_only=True)
     termination_a = serializers.SerializerMethodField(read_only=True)
     termination_b = serializers.SerializerMethodField(read_only=True)
     termination_b = serializers.SerializerMethodField(read_only=True)
     status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
     status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
-    length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
+    length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Cable
         model = Cable
@@ -691,20 +691,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
         default=None
         default=None
     )
     )
     type = ChoiceField(
     type = ChoiceField(
-        choices=POWERFEED_TYPE_CHOICES,
-        default=POWERFEED_TYPE_PRIMARY
+        choices=PowerFeedTypeChoices,
+        default=PowerFeedTypeChoices.TYPE_PRIMARY
     )
     )
     status = ChoiceField(
     status = ChoiceField(
-        choices=POWERFEED_STATUS_CHOICES,
-        default=POWERFEED_STATUS_ACTIVE
+        choices=PowerFeedStatusChoices,
+        default=PowerFeedStatusChoices.STATUS_ACTIVE
     )
     )
     supply = ChoiceField(
     supply = ChoiceField(
-        choices=POWERFEED_SUPPLY_CHOICES,
-        default=POWERFEED_SUPPLY_AC
+        choices=PowerFeedSupplyChoices,
+        default=PowerFeedSupplyChoices.SUPPLY_AC
     )
     )
     phase = ChoiceField(
     phase = ChoiceField(
-        choices=POWERFEED_PHASE_CHOICES,
-        default=POWERFEED_PHASE_SINGLE
+        choices=PowerFeedPhaseChoices,
+        default=PowerFeedPhaseChoices.PHASE_SINGLE
     )
     )
     tags = TagListSerializerField(
     tags = TagListSerializerField(
         required=False
         required=False

+ 493 - 120
netbox/dcim/choices.py

@@ -1,14 +1,187 @@
-from .constants import *
+from utilities.choices import ChoiceSet
 
 
 
 
 #
 #
-# Console port type values
+# Sites
 #
 #
 
 
-class ConsolePortTypes:
-    """
-    ConsolePort/ConsoleServerPort.type slugs
-    """
+class SiteStatusChoices(ChoiceSet):
+
+    STATUS_ACTIVE = 'active'
+    STATUS_PLANNED = 'planned'
+    STATUS_RETIRED = 'retired'
+
+    CHOICES = (
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_PLANNED, 'Planned'),
+        (STATUS_RETIRED, 'Retired'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_ACTIVE: 1,
+        STATUS_PLANNED: 2,
+        STATUS_RETIRED: 4,
+    }
+
+
+#
+# Racks
+#
+
+class RackTypeChoices(ChoiceSet):
+
+    TYPE_2POST = '2-post-frame'
+    TYPE_4POST = '4-post-frame'
+    TYPE_CABINET = '4-post-cabinet'
+    TYPE_WALLFRAME = 'wall-frame'
+    TYPE_WALLCABINET = 'wall-cabinet'
+
+    CHOICES = (
+        (TYPE_2POST, '2-post frame'),
+        (TYPE_4POST, '4-post frame'),
+        (TYPE_CABINET, '4-post cabinet'),
+        (TYPE_WALLFRAME, 'Wall-mounted frame'),
+        (TYPE_WALLCABINET, 'Wall-mounted cabinet'),
+    )
+
+    LEGACY_MAP = {
+        TYPE_2POST: 100,
+        TYPE_4POST: 200,
+        TYPE_CABINET: 300,
+        TYPE_WALLFRAME: 1000,
+        TYPE_WALLCABINET: 1100,
+    }
+
+
+class RackWidthChoices(ChoiceSet):
+
+    WIDTH_19IN = 19
+    WIDTH_23IN = 23
+
+    CHOICES = (
+        (WIDTH_19IN, '19 inches'),
+        (WIDTH_23IN, '23 inches'),
+    )
+
+
+class RackStatusChoices(ChoiceSet):
+
+    STATUS_RESERVED = 'reserved'
+    STATUS_AVAILABLE = 'available'
+    STATUS_PLANNED = 'planned'
+    STATUS_ACTIVE = 'active'
+    STATUS_DEPRECATED = 'deprecated'
+
+    CHOICES = (
+        (STATUS_RESERVED, 'Reserved'),
+        (STATUS_AVAILABLE, 'Available'),
+        (STATUS_PLANNED, 'Planned'),
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_DEPRECATED, 'Deprecated'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_RESERVED: 0,
+        STATUS_AVAILABLE: 1,
+        STATUS_PLANNED: 2,
+        STATUS_ACTIVE: 3,
+        STATUS_DEPRECATED: 4,
+    }
+
+
+class RackDimensionUnitChoices(ChoiceSet):
+
+    UNIT_MILLIMETER = 'mm'
+    UNIT_INCH = 'in'
+
+    CHOICES = (
+        (UNIT_MILLIMETER, 'Millimeters'),
+        (UNIT_INCH, 'Inches'),
+    )
+
+    LEGACY_MAP = {
+        UNIT_MILLIMETER: 1000,
+        UNIT_INCH: 2000,
+    }
+
+
+#
+# DeviceTypes
+#
+
+class SubdeviceRoleChoices(ChoiceSet):
+
+    ROLE_PARENT = 'parent'
+    ROLE_CHILD = 'child'
+
+    CHOICES = (
+        (ROLE_PARENT, 'Parent'),
+        (ROLE_CHILD, 'Child'),
+    )
+
+    LEGACY_MAP = {
+        ROLE_PARENT: True,
+        ROLE_CHILD: False,
+    }
+
+
+#
+# Devices
+#
+
+class DeviceFaceChoices(ChoiceSet):
+
+    FACE_FRONT = 'front'
+    FACE_REAR = 'rear'
+
+    CHOICES = (
+        (FACE_FRONT, 'Front'),
+        (FACE_REAR, 'Rear'),
+    )
+
+    LEGACY_MAP = {
+        FACE_FRONT: 0,
+        FACE_REAR: 1,
+    }
+
+
+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
+#
+
+class ConsolePortTypeChoices(ChoiceSet):
+
     TYPE_DE9 = 'de-9'
     TYPE_DE9 = 'de-9'
     TYPE_DB25 = 'db-25'
     TYPE_DB25 = 'db-25'
     TYPE_RJ45 = 'rj-45'
     TYPE_RJ45 = 'rj-45'
@@ -43,10 +216,11 @@ class ConsolePortTypes:
 
 
 
 
 #
 #
-# Power port types
+# PowerPorts
 #
 #
 
 
-class PowerPortTypes:
+class PowerPortTypeChoices(ChoiceSet):
+
     # TODO: Add more power port types
     # TODO: Add more power port types
     # IEC 60320
     # IEC 60320
     TYPE_IEC_C6 = 'iec-60320-c6'
     TYPE_IEC_C6 = 'iec-60320-c6'
@@ -130,10 +304,11 @@ class PowerPortTypes:
 
 
 
 
 #
 #
-# Power outlet types
+# PowerOutlets
 #
 #
 
 
-class PowerOutletTypes:
+class PowerOutletTypeChoices(ChoiceSet):
+
     # TODO: Add more power outlet types
     # TODO: Add more power outlet types
     # IEC 60320
     # IEC 60320
     TYPE_IEC_C5 = 'iec-60320-c5'
     TYPE_IEC_C5 = 'iec-60320-c5'
@@ -216,14 +391,31 @@ class PowerOutletTypes:
     )
     )
 
 
 
 
+class PowerOutletFeedLegChoices(ChoiceSet):
+
+    FEED_LEG_A = 'A'
+    FEED_LEG_B = 'B'
+    FEED_LEG_C = 'C'
+
+    CHOICES = (
+        (FEED_LEG_A, 'A'),
+        (FEED_LEG_B, 'B'),
+        (FEED_LEG_C, 'C'),
+    )
+
+    LEGACY_MAP = {
+        FEED_LEG_A: 1,
+        FEED_LEG_B: 2,
+        FEED_LEG_C: 3,
+    }
+
+
 #
 #
-# Interface type values
+# Interfaces
 #
 #
 
 
-class InterfaceTypes:
-    """
-    Interface.type slugs
-    """
+class InterfaceTypeChoices(ChoiceSet):
+
     # Virtual
     # Virtual
     TYPE_VIRTUAL = 'virtual'
     TYPE_VIRTUAL = 'virtual'
     TYPE_LAG = 'lag'
     TYPE_LAG = 'lag'
@@ -315,7 +507,7 @@ class InterfaceTypes:
     # Other
     # Other
     TYPE_OTHER = 'other'
     TYPE_OTHER = 'other'
 
 
-    TYPE_CHOICES = (
+    CHOICES = (
         (
         (
             'Virtual interfaces',
             'Virtual interfaces',
             (
             (
@@ -444,93 +636,105 @@ class InterfaceTypes:
         ),
         ),
     )
     )
 
 
-    @classmethod
-    def slug_to_integer(cls, slug):
-        """
-        Provide backward-compatible mapping of the type slug to integer.
-        """
-        return {
-            # Slug: integer
-            cls.TYPE_VIRTUAL: IFACE_TYPE_VIRTUAL,
-            cls.TYPE_LAG: IFACE_TYPE_LAG,
-            cls.TYPE_100ME_FIXED: IFACE_TYPE_100ME_FIXED,
-            cls.TYPE_1GE_FIXED: IFACE_TYPE_1GE_FIXED,
-            cls.TYPE_1GE_GBIC: IFACE_TYPE_1GE_GBIC,
-            cls.TYPE_1GE_SFP: IFACE_TYPE_1GE_SFP,
-            cls.TYPE_2GE_FIXED: IFACE_TYPE_2GE_FIXED,
-            cls.TYPE_5GE_FIXED: IFACE_TYPE_5GE_FIXED,
-            cls.TYPE_10GE_FIXED: IFACE_TYPE_10GE_FIXED,
-            cls.TYPE_10GE_CX4: IFACE_TYPE_10GE_CX4,
-            cls.TYPE_10GE_SFP_PLUS: IFACE_TYPE_10GE_SFP_PLUS,
-            cls.TYPE_10GE_XFP: IFACE_TYPE_10GE_XFP,
-            cls.TYPE_10GE_XENPAK: IFACE_TYPE_10GE_XENPAK,
-            cls.TYPE_10GE_X2: IFACE_TYPE_10GE_X2,
-            cls.TYPE_25GE_SFP28: IFACE_TYPE_25GE_SFP28,
-            cls.TYPE_40GE_QSFP_PLUS: IFACE_TYPE_40GE_QSFP_PLUS,
-            cls.TYPE_50GE_QSFP28: IFACE_TYPE_50GE_QSFP28,
-            cls.TYPE_100GE_CFP: IFACE_TYPE_100GE_CFP,
-            cls.TYPE_100GE_CFP2: IFACE_TYPE_100GE_CFP2,
-            cls.TYPE_100GE_CFP4: IFACE_TYPE_100GE_CFP4,
-            cls.TYPE_100GE_CPAK: IFACE_TYPE_100GE_CPAK,
-            cls.TYPE_100GE_QSFP28: IFACE_TYPE_100GE_QSFP28,
-            cls.TYPE_200GE_CFP2: IFACE_TYPE_200GE_CFP2,
-            cls.TYPE_200GE_QSFP56: IFACE_TYPE_200GE_QSFP56,
-            cls.TYPE_400GE_QSFP_DD: IFACE_TYPE_400GE_QSFP_DD,
-            cls.TYPE_80211A: IFACE_TYPE_80211A,
-            cls.TYPE_80211G: IFACE_TYPE_80211G,
-            cls.TYPE_80211N: IFACE_TYPE_80211N,
-            cls.TYPE_80211AC: IFACE_TYPE_80211AC,
-            cls.TYPE_80211AD: IFACE_TYPE_80211AD,
-            cls.TYPE_GSM: IFACE_TYPE_GSM,
-            cls.TYPE_CDMA: IFACE_TYPE_CDMA,
-            cls.TYPE_LTE: IFACE_TYPE_LTE,
-            cls.TYPE_SONET_OC3: IFACE_TYPE_SONET_OC3,
-            cls.TYPE_SONET_OC12: IFACE_TYPE_SONET_OC12,
-            cls.TYPE_SONET_OC48: IFACE_TYPE_SONET_OC48,
-            cls.TYPE_SONET_OC192: IFACE_TYPE_SONET_OC192,
-            cls.TYPE_SONET_OC768: IFACE_TYPE_SONET_OC768,
-            cls.TYPE_SONET_OC1920: IFACE_TYPE_SONET_OC1920,
-            cls.TYPE_SONET_OC3840: IFACE_TYPE_SONET_OC3840,
-            cls.TYPE_1GFC_SFP: IFACE_TYPE_1GFC_SFP,
-            cls.TYPE_2GFC_SFP: IFACE_TYPE_2GFC_SFP,
-            cls.TYPE_4GFC_SFP: IFACE_TYPE_4GFC_SFP,
-            cls.TYPE_8GFC_SFP_PLUS: IFACE_TYPE_8GFC_SFP_PLUS,
-            cls.TYPE_16GFC_SFP_PLUS: IFACE_TYPE_16GFC_SFP_PLUS,
-            cls.TYPE_32GFC_SFP28: IFACE_TYPE_32GFC_SFP28,
-            cls.TYPE_128GFC_QSFP28: IFACE_TYPE_128GFC_QSFP28,
-            cls.TYPE_INFINIBAND_SDR: IFACE_TYPE_INFINIBAND_SDR,
-            cls.TYPE_INFINIBAND_DDR: IFACE_TYPE_INFINIBAND_DDR,
-            cls.TYPE_INFINIBAND_QDR: IFACE_TYPE_INFINIBAND_QDR,
-            cls.TYPE_INFINIBAND_FDR10: IFACE_TYPE_INFINIBAND_FDR10,
-            cls.TYPE_INFINIBAND_FDR: IFACE_TYPE_INFINIBAND_FDR,
-            cls.TYPE_INFINIBAND_EDR: IFACE_TYPE_INFINIBAND_EDR,
-            cls.TYPE_INFINIBAND_HDR: IFACE_TYPE_INFINIBAND_HDR,
-            cls.TYPE_INFINIBAND_NDR: IFACE_TYPE_INFINIBAND_NDR,
-            cls.TYPE_INFINIBAND_XDR: IFACE_TYPE_INFINIBAND_XDR,
-            cls.TYPE_T1: IFACE_TYPE_T1,
-            cls.TYPE_E1: IFACE_TYPE_E1,
-            cls.TYPE_T3: IFACE_TYPE_T3,
-            cls.TYPE_E3: IFACE_TYPE_E3,
-            cls.TYPE_STACKWISE: IFACE_TYPE_STACKWISE,
-            cls.TYPE_STACKWISE_PLUS: IFACE_TYPE_STACKWISE_PLUS,
-            cls.TYPE_FLEXSTACK: IFACE_TYPE_FLEXSTACK,
-            cls.TYPE_FLEXSTACK_PLUS: IFACE_TYPE_FLEXSTACK_PLUS,
-            cls.TYPE_JUNIPER_VCP: IFACE_TYPE_JUNIPER_VCP,
-            cls.TYPE_SUMMITSTACK: IFACE_TYPE_SUMMITSTACK,
-            cls.TYPE_SUMMITSTACK128: IFACE_TYPE_SUMMITSTACK128,
-            cls.TYPE_SUMMITSTACK256: IFACE_TYPE_SUMMITSTACK256,
-            cls.TYPE_SUMMITSTACK512: IFACE_TYPE_SUMMITSTACK512,
-        }.get(slug)
+    LEGACY_MAP = {
+        TYPE_VIRTUAL: 0,
+        TYPE_LAG: 200,
+        TYPE_100ME_FIXED: 800,
+        TYPE_1GE_FIXED: 1000,
+        TYPE_1GE_GBIC: 1050,
+        TYPE_1GE_SFP: 1100,
+        TYPE_2GE_FIXED: 1120,
+        TYPE_5GE_FIXED: 1130,
+        TYPE_10GE_FIXED: 1150,
+        TYPE_10GE_CX4: 1170,
+        TYPE_10GE_SFP_PLUS: 1200,
+        TYPE_10GE_XFP: 1300,
+        TYPE_10GE_XENPAK: 1310,
+        TYPE_10GE_X2: 1320,
+        TYPE_25GE_SFP28: 1350,
+        TYPE_40GE_QSFP_PLUS: 1400,
+        TYPE_50GE_QSFP28: 1420,
+        TYPE_100GE_CFP: 1500,
+        TYPE_100GE_CFP2: 1510,
+        TYPE_100GE_CFP4: 1520,
+        TYPE_100GE_CPAK: 1550,
+        TYPE_100GE_QSFP28: 1600,
+        TYPE_200GE_CFP2: 1650,
+        TYPE_200GE_QSFP56: 1700,
+        TYPE_400GE_QSFP_DD: 1750,
+        TYPE_400GE_OSFP: 1800,
+        TYPE_80211A: 2600,
+        TYPE_80211G: 2610,
+        TYPE_80211N: 2620,
+        TYPE_80211AC: 2630,
+        TYPE_80211AD: 2640,
+        TYPE_GSM: 2810,
+        TYPE_CDMA: 2820,
+        TYPE_LTE: 2830,
+        TYPE_SONET_OC3: 6100,
+        TYPE_SONET_OC12: 6200,
+        TYPE_SONET_OC48: 6300,
+        TYPE_SONET_OC192: 6400,
+        TYPE_SONET_OC768: 6500,
+        TYPE_SONET_OC1920: 6600,
+        TYPE_SONET_OC3840: 6700,
+        TYPE_1GFC_SFP: 3010,
+        TYPE_2GFC_SFP: 3020,
+        TYPE_4GFC_SFP: 3040,
+        TYPE_8GFC_SFP_PLUS: 3080,
+        TYPE_16GFC_SFP_PLUS: 3160,
+        TYPE_32GFC_SFP28: 3320,
+        TYPE_128GFC_QSFP28: 3400,
+        TYPE_INFINIBAND_SDR: 7010,
+        TYPE_INFINIBAND_DDR: 7020,
+        TYPE_INFINIBAND_QDR: 7030,
+        TYPE_INFINIBAND_FDR10: 7040,
+        TYPE_INFINIBAND_FDR: 7050,
+        TYPE_INFINIBAND_EDR: 7060,
+        TYPE_INFINIBAND_HDR: 7070,
+        TYPE_INFINIBAND_NDR: 7080,
+        TYPE_INFINIBAND_XDR: 7090,
+        TYPE_T1: 4000,
+        TYPE_E1: 4010,
+        TYPE_T3: 4040,
+        TYPE_E3: 4050,
+        TYPE_STACKWISE: 5000,
+        TYPE_STACKWISE_PLUS: 5050,
+        TYPE_FLEXSTACK: 5100,
+        TYPE_FLEXSTACK_PLUS: 5150,
+        TYPE_JUNIPER_VCP: 5200,
+        TYPE_SUMMITSTACK: 5300,
+        TYPE_SUMMITSTACK128: 5310,
+        TYPE_SUMMITSTACK256: 5320,
+        TYPE_SUMMITSTACK512: 5330,
+    }
+
+
+class InterfaceModeChoices(ChoiceSet):
+
+    MODE_ACCESS = 'access'
+    MODE_TAGGED = 'tagged'
+    MODE_TAGGED_ALL = 'tagged-all'
+
+    CHOICES = (
+        (MODE_ACCESS, 'Access'),
+        (MODE_TAGGED, 'Tagged'),
+        (MODE_TAGGED_ALL, 'Tagged (All)'),
+    )
+
+    LEGACY_MAP = {
+        MODE_ACCESS: 100,
+        MODE_TAGGED: 200,
+        MODE_TAGGED_ALL: 300,
+    }
 
 
 
 
 #
 #
-# Port type values
+# FrontPorts/RearPorts
 #
 #
 
 
-class PortTypes:
-    """
-    FrontPort/RearPort.type slugs
-    """
+class PortTypeChoices(ChoiceSet):
+
     TYPE_8P8C = '8p8c'
     TYPE_8P8C = '8p8c'
     TYPE_110_PUNCH = '110-punch'
     TYPE_110_PUNCH = '110-punch'
     TYPE_BNC = 'bnc'
     TYPE_BNC = 'bnc'
@@ -545,7 +749,7 @@ class PortTypes:
     TYPE_LSH = 'lsh'
     TYPE_LSH = 'lsh'
     TYPE_LSH_APC = 'lsh-apc'
     TYPE_LSH_APC = 'lsh-apc'
 
 
-    TYPE_CHOICES = (
+    CHOICES = (
         (
         (
             'Copper',
             'Copper',
             (
             (
@@ -571,24 +775,193 @@ class PortTypes:
         )
         )
     )
     )
 
 
-    @classmethod
-    def slug_to_integer(cls, slug):
-        """
-        Provide backward-compatible mapping of the type slug to integer.
-        """
-        return {
-            # Slug: integer
-            cls.TYPE_8P8C: PORT_TYPE_8P8C,
-            cls.TYPE_110_PUNCH: PORT_TYPE_8P8C,
-            cls.TYPE_BNC: PORT_TYPE_BNC,
-            cls.TYPE_ST: PORT_TYPE_ST,
-            cls.TYPE_SC: PORT_TYPE_SC,
-            cls.TYPE_SC_APC: PORT_TYPE_SC_APC,
-            cls.TYPE_FC: PORT_TYPE_FC,
-            cls.TYPE_LC: PORT_TYPE_LC,
-            cls.TYPE_LC_APC: PORT_TYPE_LC_APC,
-            cls.TYPE_MTRJ: PORT_TYPE_MTRJ,
-            cls.TYPE_MPO: PORT_TYPE_MPO,
-            cls.TYPE_LSH: PORT_TYPE_LSH,
-            cls.TYPE_LSH_APC: PORT_TYPE_LSH_APC,
-        }.get(slug)
+    LEGACY_MAP = {
+        TYPE_8P8C: 1000,
+        TYPE_110_PUNCH: 1100,
+        TYPE_BNC: 1200,
+        TYPE_ST: 2000,
+        TYPE_SC: 2100,
+        TYPE_SC_APC: 2110,
+        TYPE_FC: 2200,
+        TYPE_LC: 2300,
+        TYPE_LC_APC: 2310,
+        TYPE_MTRJ: 2400,
+        TYPE_MPO: 2500,
+        TYPE_LSH: 2600,
+        TYPE_LSH_APC: 2610,
+    }
+
+
+#
+# Cables
+#
+
+class CableTypeChoices(ChoiceSet):
+
+    TYPE_CAT3 = 'cat3'
+    TYPE_CAT5 = 'cat5'
+    TYPE_CAT5E = 'cat5e'
+    TYPE_CAT6 = 'cat6'
+    TYPE_CAT6A = 'cat6a'
+    TYPE_CAT7 = 'cat7'
+    TYPE_DAC_ACTIVE = 'dac-active'
+    TYPE_DAC_PASSIVE = 'dac-passive'
+    TYPE_COAXIAL = 'coaxial'
+    TYPE_MMF = 'mmf'
+    TYPE_MMF_OM1 = 'mmf-om1'
+    TYPE_MMF_OM2 = 'mmf-om2'
+    TYPE_MMF_OM3 = 'mmf-om3'
+    TYPE_MMF_OM4 = 'mmf-om4'
+    TYPE_SMF = 'smf'
+    TYPE_SMF_OS1 = 'smf-os1'
+    TYPE_SMF_OS2 = 'smf-os2'
+    TYPE_AOC = 'aoc'
+    TYPE_POWER = 'power'
+
+    CHOICES = (
+        (
+            'Copper', (
+                (TYPE_CAT3, 'CAT3'),
+                (TYPE_CAT5, 'CAT5'),
+                (TYPE_CAT5E, 'CAT5e'),
+                (TYPE_CAT6, 'CAT6'),
+                (TYPE_CAT6A, 'CAT6a'),
+                (TYPE_CAT7, 'CAT7'),
+                (TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
+                (TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
+                (TYPE_COAXIAL, 'Coaxial'),
+            ),
+        ),
+        (
+            'Fiber', (
+                (TYPE_MMF, 'Multimode Fiber'),
+                (TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
+                (TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
+                (TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
+                (TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
+                (TYPE_SMF, 'Singlemode Fiber'),
+                (TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
+                (TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
+                (TYPE_AOC, 'Active Optical Cabling (AOC)'),
+            ),
+        ),
+        (TYPE_POWER, 'Power'),
+    )
+
+    LEGACY_MAP = {
+        TYPE_CAT3: 1300,
+        TYPE_CAT5: 1500,
+        TYPE_CAT5E: 1510,
+        TYPE_CAT6: 1600,
+        TYPE_CAT6A: 1610,
+        TYPE_CAT7: 1700,
+        TYPE_DAC_ACTIVE: 1800,
+        TYPE_DAC_PASSIVE: 1810,
+        TYPE_COAXIAL: 1900,
+        TYPE_MMF: 3000,
+        TYPE_MMF_OM1: 3010,
+        TYPE_MMF_OM2: 3020,
+        TYPE_MMF_OM3: 3030,
+        TYPE_MMF_OM4: 3040,
+        TYPE_SMF: 3500,
+        TYPE_SMF_OS1: 3510,
+        TYPE_SMF_OS2: 3520,
+        TYPE_AOC: 3800,
+        TYPE_POWER: 5000,
+    }
+
+
+class CableLengthUnitChoices(ChoiceSet):
+
+    UNIT_METER = 'm'
+    UNIT_CENTIMETER = 'cm'
+    UNIT_FOOT = 'ft'
+    UNIT_INCH = 'in'
+
+    CHOICES = (
+        (UNIT_METER, 'Meters'),
+        (UNIT_CENTIMETER, 'Centimeters'),
+        (UNIT_FOOT, 'Feet'),
+        (UNIT_INCH, 'Inches'),
+    )
+
+    LEGACY_MAP = {
+        UNIT_METER: 1200,
+        UNIT_CENTIMETER: 1100,
+        UNIT_FOOT: 2100,
+        UNIT_INCH: 2000,
+    }
+
+
+#
+# PowerFeeds
+#
+
+class PowerFeedStatusChoices(ChoiceSet):
+
+    STATUS_OFFLINE = 'offline'
+    STATUS_ACTIVE = 'active'
+    STATUS_PLANNED = 'planned'
+    STATUS_FAILED = 'failed'
+
+    CHOICES = (
+        (STATUS_OFFLINE, 'Offline'),
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_PLANNED, 'Planned'),
+        (STATUS_FAILED, 'Failed'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_OFFLINE: 0,
+        STATUS_ACTIVE: 1,
+        STATUS_PLANNED: 2,
+        STATUS_FAILED: 4,
+    }
+
+
+class PowerFeedTypeChoices(ChoiceSet):
+
+    TYPE_PRIMARY = 'primary'
+    TYPE_REDUNDANT = 'redundant'
+
+    CHOICES = (
+        (TYPE_PRIMARY, 'Primary'),
+        (TYPE_REDUNDANT, 'Redundant'),
+    )
+
+    LEGACY_MAP = {
+        TYPE_PRIMARY: 1,
+        TYPE_REDUNDANT: 2,
+    }
+
+
+class PowerFeedSupplyChoices(ChoiceSet):
+
+    SUPPLY_AC = 'ac'
+    SUPPLY_DC = 'dc'
+
+    CHOICES = (
+        (SUPPLY_AC, 'AC'),
+        (SUPPLY_DC, 'DC'),
+    )
+
+    LEGACY_MAP = {
+        SUPPLY_AC: 1,
+        SUPPLY_DC: 2,
+    }
+
+
+class PowerFeedPhaseChoices(ChoiceSet):
+
+    PHASE_SINGLE = 'single-phase'
+    PHASE_3PHASE = 'three-phase'
+
+    CHOICES = (
+        (PHASE_SINGLE, 'Single phase'),
+        (PHASE_3PHASE, 'Three-phase'),
+    )
+
+    LEGACY_MAP = {
+        PHASE_SINGLE: 1,
+        PHASE_3PHASE: 3,
+    }

+ 9 - 469
netbox/dcim/constants.py

@@ -1,381 +1,25 @@
-# Rack types
-RACK_TYPE_2POST = 100
-RACK_TYPE_4POST = 200
-RACK_TYPE_CABINET = 300
-RACK_TYPE_WALLFRAME = 1000
-RACK_TYPE_WALLCABINET = 1100
-RACK_TYPE_CHOICES = (
-    (RACK_TYPE_2POST, '2-post frame'),
-    (RACK_TYPE_4POST, '4-post frame'),
-    (RACK_TYPE_CABINET, '4-post cabinet'),
-    (RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
-    (RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
-)
+from .choices import InterfaceTypeChoices
 
 
-# Rack widths
-RACK_WIDTH_19IN = 19
-RACK_WIDTH_23IN = 23
-RACK_WIDTH_CHOICES = (
-    (RACK_WIDTH_19IN, '19 inches'),
-    (RACK_WIDTH_23IN, '23 inches'),
-)
-
-# Rack faces
-RACK_FACE_FRONT = 0
-RACK_FACE_REAR = 1
-RACK_FACE_CHOICES = [
-    [RACK_FACE_FRONT, 'Front'],
-    [RACK_FACE_REAR, 'Rear'],
-]
-
-# Rack statuses
-RACK_STATUS_RESERVED = 0
-RACK_STATUS_AVAILABLE = 1
-RACK_STATUS_PLANNED = 2
-RACK_STATUS_ACTIVE = 3
-RACK_STATUS_DEPRECATED = 4
-RACK_STATUS_CHOICES = [
-    [RACK_STATUS_ACTIVE, 'Active'],
-    [RACK_STATUS_PLANNED, 'Planned'],
-    [RACK_STATUS_RESERVED, 'Reserved'],
-    [RACK_STATUS_AVAILABLE, 'Available'],
-    [RACK_STATUS_DEPRECATED, 'Deprecated'],
-]
-
-# Device rack position
-DEVICE_POSITION_CHOICES = [
-    # Rack.u_height is limited to 100
-    (i, 'Unit {}'.format(i)) for i in range(1, 101)
-]
-
-# Parent/child device roles
-SUBDEVICE_ROLE_PARENT = True
-SUBDEVICE_ROLE_CHILD = False
-SUBDEVICE_ROLE_CHOICES = (
-    (None, 'None'),
-    (SUBDEVICE_ROLE_PARENT, 'Parent'),
-    (SUBDEVICE_ROLE_CHILD, 'Child'),
-)
 
 
 #
 #
-# Numeric interface types
+# Interface type groups
 #
 #
 
 
-# Virtual
-IFACE_TYPE_VIRTUAL = 0
-IFACE_TYPE_LAG = 200
-# Ethernet
-IFACE_TYPE_100ME_FIXED = 800
-IFACE_TYPE_1GE_FIXED = 1000
-IFACE_TYPE_1GE_GBIC = 1050
-IFACE_TYPE_1GE_SFP = 1100
-IFACE_TYPE_2GE_FIXED = 1120
-IFACE_TYPE_5GE_FIXED = 1130
-IFACE_TYPE_10GE_FIXED = 1150
-IFACE_TYPE_10GE_CX4 = 1170
-IFACE_TYPE_10GE_SFP_PLUS = 1200
-IFACE_TYPE_10GE_XFP = 1300
-IFACE_TYPE_10GE_XENPAK = 1310
-IFACE_TYPE_10GE_X2 = 1320
-IFACE_TYPE_25GE_SFP28 = 1350
-IFACE_TYPE_40GE_QSFP_PLUS = 1400
-IFACE_TYPE_50GE_QSFP28 = 1420
-IFACE_TYPE_100GE_CFP = 1500
-IFACE_TYPE_100GE_CFP2 = 1510
-IFACE_TYPE_100GE_CFP4 = 1520
-IFACE_TYPE_100GE_CPAK = 1550
-IFACE_TYPE_100GE_QSFP28 = 1600
-IFACE_TYPE_200GE_CFP2 = 1650
-IFACE_TYPE_200GE_QSFP56 = 1700
-IFACE_TYPE_400GE_QSFP_DD = 1750
-IFACE_TYPE_400GE_OSFP = 1800
-# Wireless
-IFACE_TYPE_80211A = 2600
-IFACE_TYPE_80211G = 2610
-IFACE_TYPE_80211N = 2620
-IFACE_TYPE_80211AC = 2630
-IFACE_TYPE_80211AD = 2640
-# Cellular
-IFACE_TYPE_GSM = 2810
-IFACE_TYPE_CDMA = 2820
-IFACE_TYPE_LTE = 2830
-# SONET
-IFACE_TYPE_SONET_OC3 = 6100
-IFACE_TYPE_SONET_OC12 = 6200
-IFACE_TYPE_SONET_OC48 = 6300
-IFACE_TYPE_SONET_OC192 = 6400
-IFACE_TYPE_SONET_OC768 = 6500
-IFACE_TYPE_SONET_OC1920 = 6600
-IFACE_TYPE_SONET_OC3840 = 6700
-# Fibrechannel
-IFACE_TYPE_1GFC_SFP = 3010
-IFACE_TYPE_2GFC_SFP = 3020
-IFACE_TYPE_4GFC_SFP = 3040
-IFACE_TYPE_8GFC_SFP_PLUS = 3080
-IFACE_TYPE_16GFC_SFP_PLUS = 3160
-IFACE_TYPE_32GFC_SFP28 = 3320
-IFACE_TYPE_128GFC_QSFP28 = 3400
-# InfiniBand
-IFACE_TYPE_INFINIBAND_SDR = 7010
-IFACE_TYPE_INFINIBAND_DDR = 7020
-IFACE_TYPE_INFINIBAND_QDR = 7030
-IFACE_TYPE_INFINIBAND_FDR10 = 7040
-IFACE_TYPE_INFINIBAND_FDR = 7050
-IFACE_TYPE_INFINIBAND_EDR = 7060
-IFACE_TYPE_INFINIBAND_HDR = 7070
-IFACE_TYPE_INFINIBAND_NDR = 7080
-IFACE_TYPE_INFINIBAND_XDR = 7090
-# Serial
-IFACE_TYPE_T1 = 4000
-IFACE_TYPE_E1 = 4010
-IFACE_TYPE_T3 = 4040
-IFACE_TYPE_E3 = 4050
-# Stacking
-IFACE_TYPE_STACKWISE = 5000
-IFACE_TYPE_STACKWISE_PLUS = 5050
-IFACE_TYPE_FLEXSTACK = 5100
-IFACE_TYPE_FLEXSTACK_PLUS = 5150
-IFACE_TYPE_JUNIPER_VCP = 5200
-IFACE_TYPE_SUMMITSTACK = 5300
-IFACE_TYPE_SUMMITSTACK128 = 5310
-IFACE_TYPE_SUMMITSTACK256 = 5320
-IFACE_TYPE_SUMMITSTACK512 = 5330
-
-# Other
-IFACE_TYPE_OTHER = 32767
-
-IFACE_TYPE_CHOICES = [
-    [
-        'Virtual interfaces',
-        [
-            [IFACE_TYPE_VIRTUAL, 'Virtual'],
-            [IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'],
-        ],
-    ],
-    [
-        'Ethernet (fixed)',
-        [
-            [IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
-            [IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'],
-            [IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
-            [IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'],
-            [IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
-            [IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'],
-        ]
-    ],
-    [
-        'Ethernet (modular)',
-        [
-            [IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'],
-            [IFACE_TYPE_1GE_SFP, 'SFP (1GE)'],
-            [IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'],
-            [IFACE_TYPE_10GE_XFP, 'XFP (10GE)'],
-            [IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'],
-            [IFACE_TYPE_10GE_X2, 'X2 (10GE)'],
-            [IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'],
-            [IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
-            [IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'],
-            [IFACE_TYPE_100GE_CFP, 'CFP (100GE)'],
-            [IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'],
-            [IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'],
-            [IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'],
-            [IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'],
-            [IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'],
-            [IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'],
-            [IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
-            [IFACE_TYPE_400GE_OSFP, 'OSFP (400GE)'],
-        ]
-    ],
-    [
-        'Wireless',
-        [
-            [IFACE_TYPE_80211A, 'IEEE 802.11a'],
-            [IFACE_TYPE_80211G, 'IEEE 802.11b/g'],
-            [IFACE_TYPE_80211N, 'IEEE 802.11n'],
-            [IFACE_TYPE_80211AC, 'IEEE 802.11ac'],
-            [IFACE_TYPE_80211AD, 'IEEE 802.11ad'],
-        ]
-    ],
-    [
-        'Cellular',
-        [
-            [IFACE_TYPE_GSM, 'GSM'],
-            [IFACE_TYPE_CDMA, 'CDMA'],
-            [IFACE_TYPE_LTE, 'LTE'],
-        ]
-    ],
-    [
-        'SONET',
-        [
-            [IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'],
-            [IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'],
-            [IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'],
-            [IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'],
-            [IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'],
-            [IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'],
-            [IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'],
-        ]
-    ],
-    [
-        'FibreChannel',
-        [
-            [IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'],
-            [IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'],
-            [IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'],
-            [IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
-            [IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
-            [IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'],
-            [IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
-        ]
-    ],
-    [
-        'InfiniBand',
-        [
-            [IFACE_TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'],
-            [IFACE_TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'],
-        ]
-    ],
-    [
-        'Serial',
-        [
-            [IFACE_TYPE_T1, 'T1 (1.544 Mbps)'],
-            [IFACE_TYPE_E1, 'E1 (2.048 Mbps)'],
-            [IFACE_TYPE_T3, 'T3 (45 Mbps)'],
-            [IFACE_TYPE_E3, 'E3 (34 Mbps)'],
-        ]
-    ],
-    [
-        'Stacking',
-        [
-            [IFACE_TYPE_STACKWISE, 'Cisco StackWise'],
-            [IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'],
-            [IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'],
-            [IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
-            [IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'],
-            [IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'],
-            [IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'],
-            [IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'],
-            [IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'],
-        ]
-    ],
-    [
-        'Other',
-        [
-            [IFACE_TYPE_OTHER, 'Other'],
-        ]
-    ],
-]
-
 VIRTUAL_IFACE_TYPES = [
 VIRTUAL_IFACE_TYPES = [
-    IFACE_TYPE_VIRTUAL,
-    IFACE_TYPE_LAG,
+    InterfaceTypeChoices.TYPE_VIRTUAL,
+    InterfaceTypeChoices.TYPE_LAG,
 ]
 ]
 
 
 WIRELESS_IFACE_TYPES = [
 WIRELESS_IFACE_TYPES = [
-    IFACE_TYPE_80211A,
-    IFACE_TYPE_80211G,
-    IFACE_TYPE_80211N,
-    IFACE_TYPE_80211AC,
-    IFACE_TYPE_80211AD,
+    InterfaceTypeChoices.TYPE_80211A,
+    InterfaceTypeChoices.TYPE_80211G,
+    InterfaceTypeChoices.TYPE_80211N,
+    InterfaceTypeChoices.TYPE_80211AC,
+    InterfaceTypeChoices.TYPE_80211AD,
 ]
 ]
 
 
 NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 
 
-IFACE_MODE_ACCESS = 100
-IFACE_MODE_TAGGED = 200
-IFACE_MODE_TAGGED_ALL = 300
-IFACE_MODE_CHOICES = [
-    [IFACE_MODE_ACCESS, 'Access'],
-    [IFACE_MODE_TAGGED, 'Tagged'],
-    [IFACE_MODE_TAGGED_ALL, 'Tagged All'],
-]
-
-# Pass-through port types
-PORT_TYPE_8P8C = 1000
-PORT_TYPE_110_PUNCH = 1100
-PORT_TYPE_BNC = 1200
-PORT_TYPE_ST = 2000
-PORT_TYPE_SC = 2100
-PORT_TYPE_SC_APC = 2110
-PORT_TYPE_FC = 2200
-PORT_TYPE_LC = 2300
-PORT_TYPE_LC_APC = 2310
-PORT_TYPE_MTRJ = 2400
-PORT_TYPE_MPO = 2500
-PORT_TYPE_LSH = 2600
-PORT_TYPE_LSH_APC = 2610
-PORT_TYPE_CHOICES = [
-    [
-        'Copper',
-        [
-            [PORT_TYPE_8P8C, '8P8C'],
-            [PORT_TYPE_110_PUNCH, '110 Punch'],
-            [PORT_TYPE_BNC, 'BNC'],
-        ],
-    ],
-    [
-        'Fiber Optic',
-        [
-            [PORT_TYPE_FC, 'FC'],
-            [PORT_TYPE_LC, 'LC'],
-            [PORT_TYPE_LC_APC, 'LC/APC'],
-            [PORT_TYPE_LSH, 'LSH'],
-            [PORT_TYPE_LSH_APC, 'LSH/APC'],
-            [PORT_TYPE_MPO, 'MPO'],
-            [PORT_TYPE_MTRJ, 'MTRJ'],
-            [PORT_TYPE_SC, 'SC'],
-            [PORT_TYPE_SC_APC, 'SC/APC'],
-            [PORT_TYPE_ST, 'ST'],
-        ]
-    ]
-]
-
-# 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'],
-]
-
-# Site statuses
-SITE_STATUS_ACTIVE = 1
-SITE_STATUS_PLANNED = 2
-SITE_STATUS_RETIRED = 4
-SITE_STATUS_CHOICES = [
-    [SITE_STATUS_ACTIVE, 'Active'],
-    [SITE_STATUS_PLANNED, 'Planned'],
-    [SITE_STATUS_RETIRED, 'Retired'],
-]
-
-# Bootstrap CSS classes for device/rack statuses
-STATUS_CLASSES = {
-    0: 'warning',
-    1: 'success',
-    2: 'info',
-    3: 'primary',
-    4: 'danger',
-    5: 'default',
-    6: 'warning',
-}
-
 # Console/power/interface connection statuses
 # Console/power/interface connection statuses
 CONNECTION_STATUS_PLANNED = False
 CONNECTION_STATUS_PLANNED = False
 CONNECTION_STATUS_CONNECTED = True
 CONNECTION_STATUS_CONNECTED = True
@@ -390,56 +34,6 @@ CABLE_TERMINATION_TYPES = [
     'circuittermination',
     'circuittermination',
 ]
 ]
 
 
-# Cable types
-CABLE_TYPE_CAT3 = 1300
-CABLE_TYPE_CAT5 = 1500
-CABLE_TYPE_CAT5E = 1510
-CABLE_TYPE_CAT6 = 1600
-CABLE_TYPE_CAT6A = 1610
-CABLE_TYPE_CAT7 = 1700
-CABLE_TYPE_DAC_ACTIVE = 1800
-CABLE_TYPE_DAC_PASSIVE = 1810
-CABLE_TYPE_COAXIAL = 1900
-CABLE_TYPE_MMF = 3000
-CABLE_TYPE_MMF_OM1 = 3010
-CABLE_TYPE_MMF_OM2 = 3020
-CABLE_TYPE_MMF_OM3 = 3030
-CABLE_TYPE_MMF_OM4 = 3040
-CABLE_TYPE_SMF = 3500
-CABLE_TYPE_SMF_OS1 = 3510
-CABLE_TYPE_SMF_OS2 = 3520
-CABLE_TYPE_AOC = 3800
-CABLE_TYPE_POWER = 5000
-CABLE_TYPE_CHOICES = (
-    (
-        'Copper', (
-            (CABLE_TYPE_CAT3, 'CAT3'),
-            (CABLE_TYPE_CAT5, 'CAT5'),
-            (CABLE_TYPE_CAT5E, 'CAT5e'),
-            (CABLE_TYPE_CAT6, 'CAT6'),
-            (CABLE_TYPE_CAT6A, 'CAT6a'),
-            (CABLE_TYPE_CAT7, 'CAT7'),
-            (CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
-            (CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
-            (CABLE_TYPE_COAXIAL, 'Coaxial'),
-        ),
-    ),
-    (
-        'Fiber', (
-            (CABLE_TYPE_MMF, 'Multimode Fiber'),
-            (CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
-            (CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
-            (CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
-            (CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
-            (CABLE_TYPE_SMF, 'Singlemode Fiber'),
-            (CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
-            (CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
-            (CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
-        ),
-    ),
-    (CABLE_TYPE_POWER, 'Power'),
-)
-
 CABLE_TERMINATION_TYPE_CHOICES = {
 CABLE_TERMINATION_TYPE_CHOICES = {
     # (API endpoint, human-friendly name)
     # (API endpoint, human-friendly name)
     'consoleport': ('console-ports', 'Console port'),
     'consoleport': ('console-ports', 'Console port'),
@@ -461,57 +55,3 @@ COMPATIBLE_TERMINATION_TYPES = {
     'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
     'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
     'circuittermination': ['interface', 'frontport', 'rearport'],
     'circuittermination': ['interface', 'frontport', 'rearport'],
 }
 }
-
-LENGTH_UNIT_METER = 1200
-LENGTH_UNIT_CENTIMETER = 1100
-LENGTH_UNIT_MILLIMETER = 1000
-LENGTH_UNIT_FOOT = 2100
-LENGTH_UNIT_INCH = 2000
-CABLE_LENGTH_UNIT_CHOICES = (
-    (LENGTH_UNIT_METER, 'Meters'),
-    (LENGTH_UNIT_CENTIMETER, 'Centimeters'),
-    (LENGTH_UNIT_FOOT, 'Feet'),
-    (LENGTH_UNIT_INCH, 'Inches'),
-)
-RACK_DIMENSION_UNIT_CHOICES = (
-    (LENGTH_UNIT_MILLIMETER, 'Millimeters'),
-    (LENGTH_UNIT_INCH, 'Inches'),
-)
-
-# Power feeds
-POWERFEED_TYPE_PRIMARY = 1
-POWERFEED_TYPE_REDUNDANT = 2
-POWERFEED_TYPE_CHOICES = (
-    (POWERFEED_TYPE_PRIMARY, 'Primary'),
-    (POWERFEED_TYPE_REDUNDANT, 'Redundant'),
-)
-POWERFEED_SUPPLY_AC = 1
-POWERFEED_SUPPLY_DC = 2
-POWERFEED_SUPPLY_CHOICES = (
-    (POWERFEED_SUPPLY_AC, 'AC'),
-    (POWERFEED_SUPPLY_DC, 'DC'),
-)
-POWERFEED_PHASE_SINGLE = 1
-POWERFEED_PHASE_3PHASE = 3
-POWERFEED_PHASE_CHOICES = (
-    (POWERFEED_PHASE_SINGLE, 'Single phase'),
-    (POWERFEED_PHASE_3PHASE, 'Three-phase'),
-)
-POWERFEED_STATUS_OFFLINE = 0
-POWERFEED_STATUS_ACTIVE = 1
-POWERFEED_STATUS_PLANNED = 2
-POWERFEED_STATUS_FAILED = 4
-POWERFEED_STATUS_CHOICES = (
-    (POWERFEED_STATUS_ACTIVE, 'Active'),
-    (POWERFEED_STATUS_OFFLINE, 'Offline'),
-    (POWERFEED_STATUS_PLANNED, 'Planned'),
-    (POWERFEED_STATUS_FAILED, 'Failed'),
-)
-POWERFEED_LEG_A = 1
-POWERFEED_LEG_B = 2
-POWERFEED_LEG_C = 3
-POWERFEED_LEG_CHOICES = (
-    (POWERFEED_LEG_A, 'A'),
-    (POWERFEED_LEG_B, 'B'),
-    (POWERFEED_LEG_C, 'C'),
-)

+ 9 - 9
netbox/dcim/filters.py

@@ -49,7 +49,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         label='Search',
         label='Search',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=SITE_STATUS_CHOICES,
+        choices=SiteStatusChoices,
         null_value=None
         null_value=None
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
@@ -147,7 +147,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         label='Group',
         label='Group',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=RACK_STATUS_CHOICES,
+        choices=RackStatusChoices,
         null_value=None
         null_value=None
     )
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
     role_id = django_filters.ModelMultipleChoiceFilter(
@@ -511,7 +511,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
         label='Device model (slug)',
         label='Device model (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=DEVICE_STATUS_CHOICES,
+        choices=DeviceStatusChoices,
         null_value=None
         null_value=None
     )
     )
     is_full_depth = django_filters.BooleanFilter(
     is_full_depth = django_filters.BooleanFilter(
@@ -663,7 +663,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
 
 
 class ConsolePortFilter(DeviceComponentFilterSet):
 class ConsolePortFilter(DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
     )
     )
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
@@ -679,7 +679,7 @@ class ConsolePortFilter(DeviceComponentFilterSet):
 
 
 class ConsoleServerPortFilter(DeviceComponentFilterSet):
 class ConsoleServerPortFilter(DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
     )
     )
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
@@ -695,7 +695,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
 
 
 class PowerPortFilter(DeviceComponentFilterSet):
 class PowerPortFilter(DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=PowerPortTypes.CHOICES,
+        choices=PowerPortTypeChoices,
         null_value=None
         null_value=None
     )
     )
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
@@ -711,7 +711,7 @@ class PowerPortFilter(DeviceComponentFilterSet):
 
 
 class PowerOutletFilter(DeviceComponentFilterSet):
 class PowerOutletFilter(DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=PowerOutletTypes.CHOICES,
+        choices=PowerOutletTypeChoices,
         null_value=None
         null_value=None
     )
     )
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
@@ -789,7 +789,7 @@ class InterfaceFilter(django_filters.FilterSet):
         label='Assigned VID'
         label='Assigned VID'
     )
     )
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=IFACE_TYPE_CHOICES,
+        choices=InterfaceTypeChoices,
         null_value=None
         null_value=None
     )
     )
 
 
@@ -980,7 +980,7 @@ class CableFilter(django_filters.FilterSet):
         label='Search',
         label='Search',
     )
     )
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=CABLE_TYPE_CHOICES
+        choices=CableTypeChoices
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
         choices=CONNECTION_STATUS_CHOICES
         choices=CONNECTION_STATUS_CHOICES

+ 11 - 11
netbox/dcim/fixtures/dcim.json

@@ -1910,7 +1910,7 @@
         "site": 1,
         "site": 1,
         "rack": 1,
         "rack": 1,
         "position": 1,
         "position": 1,
-        "face": 0,
+        "face": "front",
         "status": true,
         "status": true,
         "primary_ip4": 1,
         "primary_ip4": 1,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -1931,7 +1931,7 @@
         "site": 1,
         "site": 1,
         "rack": 1,
         "rack": 1,
         "position": 17,
         "position": 17,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": 5,
         "primary_ip4": 5,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -1952,7 +1952,7 @@
         "site": 1,
         "site": 1,
         "rack": 1,
         "rack": 1,
         "position": 33,
         "position": 33,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -1973,7 +1973,7 @@
         "site": 1,
         "site": 1,
         "rack": 1,
         "rack": 1,
         "position": 34,
         "position": 34,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -1994,7 +1994,7 @@
         "site": 1,
         "site": 1,
         "rack": 2,
         "rack": 2,
         "position": 34,
         "position": 34,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -2015,7 +2015,7 @@
         "site": 1,
         "site": 1,
         "rack": 2,
         "rack": 2,
         "position": 33,
         "position": 33,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -2036,7 +2036,7 @@
         "site": 1,
         "site": 1,
         "rack": 2,
         "rack": 2,
         "position": 1,
         "position": 1,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": 3,
         "primary_ip4": 3,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -2057,7 +2057,7 @@
         "site": 1,
         "site": 1,
         "rack": 2,
         "rack": 2,
         "position": 17,
         "position": 17,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": 19,
         "primary_ip4": 19,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -2078,7 +2078,7 @@
         "site": 1,
         "site": 1,
         "rack": 1,
         "rack": 1,
         "position": 42,
         "position": 42,
-        "face": 0,
+        "face": "rear",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -2099,7 +2099,7 @@
         "site": 1,
         "site": 1,
         "rack": 1,
         "rack": 1,
         "position": null,
         "position": null,
-        "face": null,
+        "face": "",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,
@@ -2120,7 +2120,7 @@
         "site": 1,
         "site": 1,
         "rack": 2,
         "rack": 2,
         "position": null,
         "position": null,
-        "face": null,
+        "face": "",
         "status": true,
         "status": true,
         "primary_ip4": null,
         "primary_ip4": null,
         "primary_ip6": null,
         "primary_ip6": null,

+ 82 - 99
netbox/dcim/forms.py

@@ -93,13 +93,13 @@ class InterfaceCommonForm:
         tagged_vlans = self.cleaned_data['tagged_vlans']
         tagged_vlans = self.cleaned_data['tagged_vlans']
 
 
         # Untagged interfaces cannot be assigned tagged VLANs
         # Untagged interfaces cannot be assigned tagged VLANs
-        if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
+        if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
             raise forms.ValidationError({
             raise forms.ValidationError({
                 'mode': "An access interface cannot have tagged VLANs assigned."
                 'mode': "An access interface cannot have tagged VLANs assigned."
             })
             })
 
 
         # Remove all tagged VLAN assignments from "tagged all" interfaces
         # Remove all tagged VLAN assignments from "tagged all" interfaces
-        elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
+        elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
             self.cleaned_data['tagged_vlans'] = []
             self.cleaned_data['tagged_vlans'] = []
 
 
 
 
@@ -250,7 +250,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
 
 
 class SiteCSVForm(forms.ModelForm):
 class SiteCSVForm(forms.ModelForm):
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=SITE_STATUS_CHOICES,
+        choices=SiteStatusChoices,
         required=False,
         required=False,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
@@ -289,7 +289,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(SITE_STATUS_CHOICES),
+        choices=add_blank_choice(SiteStatusChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -338,7 +338,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         label='Search'
         label='Search'
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=SITE_STATUS_CHOICES,
+        choices=SiteStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -500,7 +500,7 @@ class RackCSVForm(forms.ModelForm):
         }
         }
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=RACK_STATUS_CHOICES,
+        choices=RackStatusChoices,
         required=False,
         required=False,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
@@ -514,19 +514,16 @@ class RackCSVForm(forms.ModelForm):
         }
         }
     )
     )
     type = CSVChoiceField(
     type = CSVChoiceField(
-        choices=RACK_TYPE_CHOICES,
+        choices=RackTypeChoices,
         required=False,
         required=False,
         help_text='Rack type'
         help_text='Rack type'
     )
     )
     width = forms.ChoiceField(
     width = forms.ChoiceField(
-        choices=(
-            (RACK_WIDTH_19IN, '19'),
-            (RACK_WIDTH_23IN, '23'),
-        ),
+        choices=RackWidthChoices,
         help_text='Rail-to-rail width (in inches)'
         help_text='Rail-to-rail width (in inches)'
     )
     )
     outer_unit = CSVChoiceField(
     outer_unit = CSVChoiceField(
-        choices=RACK_DIMENSION_UNIT_CHOICES,
+        choices=RackDimensionUnitChoices,
         required=False,
         required=False,
         help_text='Unit for outer dimensions'
         help_text='Unit for outer dimensions'
     )
     )
@@ -598,7 +595,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(RACK_STATUS_CHOICES),
+        choices=add_blank_choice(RackStatusChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -620,12 +617,12 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         required=False
         required=False
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(RACK_TYPE_CHOICES),
+        choices=add_blank_choice(RackTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     width = forms.ChoiceField(
     width = forms.ChoiceField(
-        choices=add_blank_choice(RACK_WIDTH_CHOICES),
+        choices=add_blank_choice(RackWidthChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -647,7 +644,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         min_value=1
         min_value=1
     )
     )
     outer_unit = forms.ChoiceField(
     outer_unit = forms.ChoiceField(
-        choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
+        choices=add_blank_choice(RackDimensionUnitChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -692,7 +689,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=RACK_STATUS_CHOICES,
+        choices=RackStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -909,12 +906,10 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    subdevice_role = forms.NullBooleanField(
+    subdevice_role = forms.MultipleChoiceField(
+        choices=add_blank_choice(SubdeviceRoleChoices),
         required=False,
         required=False,
-        label='Subdevice role',
-        widget=StaticSelect2(
-            choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
-        )
+        widget=StaticSelect2Multiple()
     )
     )
     console_ports = forms.NullBooleanField(
     console_ports = forms.NullBooleanField(
         required=False,
         required=False,
@@ -981,7 +976,7 @@ class ConsolePortTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
 
 
@@ -1003,7 +998,7 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypes.CHOICES),
+        choices=add_blank_choice(ConsolePortTypeChoices),
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
 
 
@@ -1025,7 +1020,7 @@ class PowerPortTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(PowerPortTypes.CHOICES),
+        choices=add_blank_choice(PowerPortTypeChoices),
         required=False
         required=False
     )
     )
     maximum_draw = forms.IntegerField(
     maximum_draw = forms.IntegerField(
@@ -1067,7 +1062,7 @@ class PowerOutletTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(PowerOutletTypes.CHOICES),
+        choices=add_blank_choice(PowerOutletTypeChoices),
         required=False
         required=False
     )
     )
     power_port = forms.ModelChoiceField(
     power_port = forms.ModelChoiceField(
@@ -1075,7 +1070,7 @@ class PowerOutletTemplateCreateForm(ComponentForm):
         required=False
         required=False
     )
     )
     feed_leg = forms.ChoiceField(
     feed_leg = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_LEG_CHOICES),
+        choices=add_blank_choice(PowerOutletFeedLegChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -1108,7 +1103,7 @@ class InterfaceTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=IFACE_TYPE_CHOICES,
+        choices=InterfaceTypeChoices,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     mgmt_only = forms.BooleanField(
     mgmt_only = forms.BooleanField(
@@ -1123,7 +1118,7 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(IFACE_TYPE_CHOICES),
+        choices=add_blank_choice(InterfaceTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -1165,7 +1160,7 @@ class FrontPortTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PORT_TYPE_CHOICES,
+        choices=PortTypeChoices,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     rear_port_set = forms.MultipleChoiceField(
     rear_port_set = forms.MultipleChoiceField(
@@ -1235,7 +1230,7 @@ class RearPortTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PORT_TYPE_CHOICES,
+        choices=PortTypeChoices,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
     positions = forms.IntegerField(
     positions = forms.IntegerField(
@@ -1334,7 +1329,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
 
 
 class InterfaceTemplateImportForm(ComponentTemplateImportForm):
 class InterfaceTemplateImportForm(ComponentTemplateImportForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=InterfaceTypes.TYPE_CHOICES
+        choices=InterfaceTypeChoices.CHOICES
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1343,15 +1338,10 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
             'device_type', 'name', 'type', 'mgmt_only',
             'device_type', 'name', 'type', 'mgmt_only',
         ]
         ]
 
 
-    def clean_type(self):
-        # Convert slug value to field integer value
-        slug = self.cleaned_data['type']
-        return InterfaceTypes.slug_to_integer(slug)
-
 
 
 class FrontPortTemplateImportForm(ComponentTemplateImportForm):
 class FrontPortTemplateImportForm(ComponentTemplateImportForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PortTypes.TYPE_CHOICES
+        choices=PortTypeChoices.CHOICES
     )
     )
     rear_port = forms.ModelChoiceField(
     rear_port = forms.ModelChoiceField(
         queryset=RearPortTemplate.objects.all(),
         queryset=RearPortTemplate.objects.all(),
@@ -1365,15 +1355,10 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
             'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
             'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
         ]
         ]
 
 
-    def clean_type(self):
-        # Convert slug value to field integer value
-        slug = self.cleaned_data['type']
-        return PortTypes.slug_to_integer(slug)
-
 
 
 class RearPortTemplateImportForm(ComponentTemplateImportForm):
 class RearPortTemplateImportForm(ComponentTemplateImportForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PortTypes.TYPE_CHOICES
+        choices=PortTypeChoices.CHOICES
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1382,11 +1367,6 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
             'device_type', 'name', 'type', 'positions',
             'device_type', 'name', 'type', 'positions',
         ]
         ]
 
 
-    def clean_type(self):
-        # Convert slug value to field integer value
-        slug = self.cleaned_data['type']
-        return PortTypes.slug_to_integer(slug)
-
 
 
 class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
 class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
 
 
@@ -1702,7 +1682,7 @@ class BaseDeviceCSVForm(forms.ModelForm):
         }
         }
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=DEVICE_STATUS_CHOICES,
+        choices=DeviceStatusChoices,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
 
 
@@ -1746,7 +1726,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
         help_text='Name of parent rack'
         help_text='Name of parent rack'
     )
     )
     face = CSVChoiceField(
     face = CSVChoiceField(
-        choices=RACK_FACE_CHOICES,
+        choices=DeviceFaceChoices,
         required=False,
         required=False,
         help_text='Mounted rack face'
         help_text='Mounted rack face'
     )
     )
@@ -1870,7 +1850,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(DEVICE_STATUS_CHOICES),
+        choices=add_blank_choice(DeviceStatusChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -1981,7 +1961,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=DEVICE_STATUS_CHOICES,
+        choices=DeviceStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -2063,7 +2043,7 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
 
 
 class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
 class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=IFACE_TYPE_CHOICES,
+        choices=InterfaceTypeChoices,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     enabled = forms.BooleanField(
     enabled = forms.BooleanField(
@@ -2115,7 +2095,7 @@ class ConsolePortCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypes.CHOICES),
+        choices=add_blank_choice(ConsolePortTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2172,7 +2152,7 @@ class ConsoleServerPortCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypes.CHOICES),
+        choices=add_blank_choice(ConsolePortTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2191,7 +2171,7 @@ class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditF
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypes.CHOICES),
+        choices=add_blank_choice(ConsolePortTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2264,7 +2244,7 @@ class PowerPortCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(PowerPortTypes.CHOICES),
+        choices=add_blank_choice(PowerPortTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2344,7 +2324,7 @@ class PowerOutletCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(PowerOutletTypes.CHOICES),
+        choices=add_blank_choice(PowerOutletTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2353,7 +2333,7 @@ class PowerOutletCreateForm(ComponentForm):
         required=False
         required=False
     )
     )
     feed_leg = forms.ChoiceField(
     feed_leg = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_LEG_CHOICES),
+        choices=add_blank_choice(PowerOutletFeedLegChoices),
         required=False
         required=False
     )
     )
     description = forms.CharField(
     description = forms.CharField(
@@ -2391,7 +2371,7 @@ class PowerOutletCSVForm(forms.ModelForm):
         }
         }
     )
     )
     feed_leg = CSVChoiceField(
     feed_leg = CSVChoiceField(
-        choices=POWERFEED_LEG_CHOICES,
+        choices=PowerOutletFeedLegChoices,
         required=False,
         required=False,
     )
     )
 
 
@@ -2428,11 +2408,11 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PowerOutletTypes.CHOICES,
+        choices=PowerOutletTypeChoices,
         required=False
         required=False
     )
     )
     feed_leg = forms.ChoiceField(
     feed_leg = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_LEG_CHOICES),
+        choices=add_blank_choice(PowerOutletFeedLegChoices),
         required=False,
         required=False,
     )
     )
     power_port = forms.ModelChoiceField(
     power_port = forms.ModelChoiceField(
@@ -2529,12 +2509,14 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         if self.is_bound:
         if self.is_bound:
             device = Device.objects.get(pk=self.data['device'])
             device = Device.objects.get(pk=self.data['device'])
             self.fields['lag'].queryset = Interface.objects.filter(
             self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
+                device__in=[device, device.get_vc_master()],
+                type=InterfaceTypeChoices.TYPE_LAG
             )
             )
         else:
         else:
             device = self.instance.device
             device = self.instance.device
             self.fields['lag'].queryset = Interface.objects.filter(
             self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
+                device__in=[self.instance.device, self.instance.device.get_vc_master()],
+                type=InterfaceTypeChoices.TYPE_LAG
             )
             )
 
 
         # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
         # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
@@ -2573,7 +2555,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=IFACE_TYPE_CHOICES,
+        choices=InterfaceTypeChoices,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
     enabled = forms.BooleanField(
     enabled = forms.BooleanField(
@@ -2605,7 +2587,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
         required=False
         required=False
     )
     )
     mode = forms.ChoiceField(
     mode = forms.ChoiceField(
-        choices=add_blank_choice(IFACE_MODE_CHOICES),
+        choices=add_blank_choice(InterfaceModeChoices),
         required=False,
         required=False,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
@@ -2642,7 +2624,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
         # Limit LAG choices to interfaces belonging to this device (or its VC master)
         # Limit LAG choices to interfaces belonging to this device (or its VC master)
         if self.parent is not None:
         if self.parent is not None:
             self.fields['lag'].queryset = Interface.objects.filter(
             self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[self.parent, self.parent.get_vc_master()], type=IFACE_TYPE_LAG
+                device__in=[self.parent, self.parent.get_vc_master()],
+                type=InterfaceTypeChoices.TYPE_LAG
             )
             )
         else:
         else:
             self.fields['lag'].queryset = Interface.objects.none()
             self.fields['lag'].queryset = Interface.objects.none()
@@ -2707,10 +2690,10 @@ class InterfaceCSVForm(forms.ModelForm):
         }
         }
     )
     )
     type = CSVChoiceField(
     type = CSVChoiceField(
-        choices=IFACE_TYPE_CHOICES,
+        choices=InterfaceTypeChoices,
     )
     )
     mode = CSVChoiceField(
     mode = CSVChoiceField(
-        choices=IFACE_MODE_CHOICES,
+        choices=InterfaceModeChoices,
         required=False,
         required=False,
     )
     )
 
 
@@ -2732,7 +2715,7 @@ class InterfaceCSVForm(forms.ModelForm):
 
 
         if device:
         if device:
             self.fields['lag'].queryset = Interface.objects.filter(
             self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
+                device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG
             )
             )
         else:
         else:
             self.fields['lag'].queryset = Interface.objects.none()
             self.fields['lag'].queryset = Interface.objects.none()
@@ -2751,7 +2734,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(IFACE_TYPE_CHOICES),
+        choices=add_blank_choice(InterfaceTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2785,7 +2768,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
         required=False
         required=False
     )
     )
     mode = forms.ChoiceField(
     mode = forms.ChoiceField(
-        choices=add_blank_choice(IFACE_MODE_CHOICES),
+        choices=add_blank_choice(InterfaceModeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -2821,7 +2804,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
         if device is not None:
         if device is not None:
             self.fields['lag'].queryset = Interface.objects.filter(
             self.fields['lag'].queryset = Interface.objects.filter(
                 device__in=[device, device.get_vc_master()],
                 device__in=[device, device.get_vc_master()],
-                type=IFACE_TYPE_LAG
+                type=InterfaceTypeChoices.TYPE_LAG
             )
             )
         else:
         else:
             self.fields['lag'].choices = []
             self.fields['lag'].choices = []
@@ -2911,7 +2894,7 @@ class FrontPortCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PORT_TYPE_CHOICES,
+        choices=PortTypeChoices,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
     rear_port_set = forms.MultipleChoiceField(
     rear_port_set = forms.MultipleChoiceField(
@@ -2983,7 +2966,7 @@ class FrontPortCSVForm(forms.ModelForm):
         }
         }
     )
     )
     type = CSVChoiceField(
     type = CSVChoiceField(
-        choices=PORT_TYPE_CHOICES,
+        choices=PortTypeChoices,
     )
     )
 
 
     class Meta:
     class Meta:
@@ -3019,7 +3002,7 @@ class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(PORT_TYPE_CHOICES),
+        choices=add_blank_choice(PortTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -3077,7 +3060,7 @@ class RearPortCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PORT_TYPE_CHOICES,
+        choices=PortTypeChoices,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
     positions = forms.IntegerField(
     positions = forms.IntegerField(
@@ -3101,7 +3084,7 @@ class RearPortCSVForm(forms.ModelForm):
         }
         }
     )
     )
     type = CSVChoiceField(
     type = CSVChoiceField(
-        choices=PORT_TYPE_CHOICES,
+        choices=PortTypeChoices,
     )
     )
 
 
     class Meta:
     class Meta:
@@ -3115,7 +3098,7 @@ class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(PORT_TYPE_CHOICES),
+        choices=add_blank_choice(PortTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -3449,12 +3432,12 @@ class CableCSVForm(forms.ModelForm):
         help_text='Connection status'
         help_text='Connection status'
     )
     )
     type = CSVChoiceField(
     type = CSVChoiceField(
-        choices=CABLE_TYPE_CHOICES,
+        choices=CableTypeChoices,
         required=False,
         required=False,
         help_text='Cable type'
         help_text='Cable type'
     )
     )
     length_unit = CSVChoiceField(
     length_unit = CSVChoiceField(
-        choices=CABLE_LENGTH_UNIT_CHOICES,
+        choices=CableLengthUnitChoices,
         required=False,
         required=False,
         help_text='Length unit'
         help_text='Length unit'
     )
     )
@@ -3534,7 +3517,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(CABLE_TYPE_CHOICES),
+        choices=add_blank_choice(CableTypeChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -3559,7 +3542,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False
         required=False
     )
     )
     length_unit = forms.ChoiceField(
     length_unit = forms.ChoiceField(
-        choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
+        choices=add_blank_choice(CableLengthUnitChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -3608,7 +3591,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
         )
         )
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
-        choices=add_blank_choice(CABLE_TYPE_CHOICES),
+        choices=add_blank_choice(CableTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -3677,7 +3660,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
             rack=device_bay.device.rack,
             rack=device_bay.device.rack,
             parent_bay__isnull=True,
             parent_bay__isnull=True,
             device_type__u_height=0,
             device_type__u_height=0,
-            device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
+            device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
         ).exclude(pk=device_bay.device.pk)
         ).exclude(pk=device_bay.device.pk)
 
 
 
 
@@ -3725,7 +3708,7 @@ class DeviceBayCSVForm(forms.ModelForm):
                 rack=device.rack,
                 rack=device.rack,
                 parent_bay__isnull=True,
                 parent_bay__isnull=True,
                 device_type__u_height=0,
                 device_type__u_height=0,
-                device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
+                device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
             ).exclude(pk=device.pk)
             ).exclude(pk=device.pk)
         else:
         else:
             self.fields['installed_device'].queryset = Interface.objects.none()
             self.fields['installed_device'].queryset = Interface.objects.none()
@@ -4214,22 +4197,22 @@ class PowerFeedCSVForm(forms.ModelForm):
         help_text="Rack name (optional)"
         help_text="Rack name (optional)"
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=POWERFEED_STATUS_CHOICES,
+        choices=PowerFeedStatusChoices,
         required=False,
         required=False,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
     type = CSVChoiceField(
     type = CSVChoiceField(
-        choices=POWERFEED_TYPE_CHOICES,
+        choices=PowerFeedTypeChoices,
         required=False,
         required=False,
         help_text='Primary or redundant'
         help_text='Primary or redundant'
     )
     )
     supply = CSVChoiceField(
     supply = CSVChoiceField(
-        choices=POWERFEED_SUPPLY_CHOICES,
+        choices=PowerFeedSupplyChoices,
         required=False,
         required=False,
         help_text='AC/DC'
         help_text='AC/DC'
     )
     )
     phase = CSVChoiceField(
     phase = CSVChoiceField(
-        choices=POWERFEED_PHASE_CHOICES,
+        choices=PowerFeedPhaseChoices,
         required=False,
         required=False,
         help_text='Single or three-phase'
         help_text='Single or three-phase'
     )
     )
@@ -4289,25 +4272,25 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
+        choices=add_blank_choice(PowerFeedStatusChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
+        choices=add_blank_choice(PowerFeedTypeChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     supply = forms.ChoiceField(
     supply = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
+        choices=add_blank_choice(PowerFeedSupplyChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     phase = forms.ChoiceField(
     phase = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
+        choices=add_blank_choice(PowerFeedPhaseChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
@@ -4368,22 +4351,22 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=POWERFEED_STATUS_CHOICES,
+        choices=PowerFeedStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
+        choices=add_blank_choice(PowerFeedTypeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     supply = forms.ChoiceField(
     supply = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
+        choices=add_blank_choice(PowerFeedSupplyChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     phase = forms.ChoiceField(
     phase = forms.ChoiceField(
-        choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
+        choices=add_blank_choice(PowerFeedPhaseChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )

+ 35 - 0
netbox/dcim/migrations/0078_3569_site_fields.py

@@ -0,0 +1,35 @@
+from django.db import migrations, models
+
+SITE_STATUS_CHOICES = (
+    (1, 'active'),
+    (2, 'planned'),
+    (4, 'retired'),
+)
+
+
+def site_status_to_slug(apps, schema_editor):
+    Site = apps.get_model('dcim', 'Site')
+    for id, slug in SITE_STATUS_CHOICES:
+        Site.objects.filter(status=str(id)).update(status=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0077_power_types'),
+    ]
+
+    operations = [
+
+        # Site.status
+        migrations.AlterField(
+            model_name='site',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=site_status_to_slug
+        ),
+
+    ]

+ 92 - 0
netbox/dcim/migrations/0079_3569_rack_fields.py

@@ -0,0 +1,92 @@
+from django.db import migrations, models
+
+RACK_TYPE_CHOICES = (
+    (100, '2-post-frame'),
+    (200, '4-post-frame'),
+    (300, '4-post-cabinet'),
+    (1000, 'wall-frame'),
+    (1100, 'wall-cabinet'),
+)
+
+RACK_STATUS_CHOICES = (
+    (0, 'reserved'),
+    (1, 'available'),
+    (2, 'planned'),
+    (3, 'active'),
+    (4, 'deprecated'),
+)
+
+RACK_DIMENSION_CHOICES = (
+    (1000, 'mm'),
+    (2000, 'in'),
+)
+
+
+def rack_type_to_slug(apps, schema_editor):
+    Rack = apps.get_model('dcim', 'Rack')
+    for id, slug in RACK_TYPE_CHOICES:
+        Rack.objects.filter(type=str(id)).update(type=slug)
+
+
+def rack_status_to_slug(apps, schema_editor):
+    Rack = apps.get_model('dcim', 'Rack')
+    for id, slug in RACK_STATUS_CHOICES:
+        Rack.objects.filter(status=str(id)).update(status=slug)
+
+
+def rack_outer_unit_to_slug(apps, schema_editor):
+    Rack = apps.get_model('dcim', 'Rack')
+    for id, slug in RACK_DIMENSION_CHOICES:
+        Rack.objects.filter(status=str(id)).update(status=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0078_3569_site_fields'),
+    ]
+
+    operations = [
+
+        # Rack.type
+        migrations.AlterField(
+            model_name='rack',
+            name='type',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=rack_type_to_slug
+        ),
+        migrations.AlterField(
+            model_name='rack',
+            name='type',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+        # Rack.status
+        migrations.AlterField(
+            model_name='rack',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=rack_status_to_slug
+        ),
+
+        # Rack.outer_unit
+        migrations.AlterField(
+            model_name='rack',
+            name='outer_unit',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=rack_outer_unit_to_slug
+        ),
+        migrations.AlterField(
+            model_name='rack',
+            name='outer_unit',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+    ]

+ 39 - 0
netbox/dcim/migrations/0080_3569_devicetype_fields.py

@@ -0,0 +1,39 @@
+from django.db import migrations, models
+
+SUBDEVICE_ROLE_CHOICES = (
+    ('true', 'parent'),
+    ('false', 'child'),
+)
+
+
+def devicetype_subdevicerole_to_slug(apps, schema_editor):
+    DeviceType = apps.get_model('dcim', 'DeviceType')
+    for boolean, slug in SUBDEVICE_ROLE_CHOICES:
+        DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0079_3569_rack_fields'),
+    ]
+
+    operations = [
+
+        # DeviceType.subdevice_role
+        migrations.AlterField(
+            model_name='devicetype',
+            name='subdevice_role',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=devicetype_subdevicerole_to_slug
+        ),
+        migrations.AlterField(
+            model_name='devicetype',
+            name='subdevice_role',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+    ]

+ 65 - 0
netbox/dcim/migrations/0081_3569_device_fields.py

@@ -0,0 +1,65 @@
+from django.db import migrations, models
+
+DEVICE_FACE_CHOICES = (
+    (0, 'front'),
+    (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')
+    for id, slug in DEVICE_FACE_CHOICES:
+        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
+
+    dependencies = [
+        ('dcim', '0080_3569_devicetype_fields'),
+    ]
+
+    operations = [
+
+        # Device.face
+        migrations.AlterField(
+            model_name='device',
+            name='face',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=device_face_to_slug
+        ),
+        migrations.AlterField(
+            model_name='device',
+            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
+        ),
+
+    ]

+ 147 - 0
netbox/dcim/migrations/0082_3569_interface_fields.py

@@ -0,0 +1,147 @@
+from django.db import migrations, models
+
+
+INTERFACE_TYPE_CHOICES = (
+    (0, 'virtual'),
+    (200, 'lag'),
+    (800, '100base-tx'),
+    (1000, '1000base-t'),
+    (1050, '1000base-x-gbic'),
+    (1100, '1000base-x-sfp'),
+    (1120, '2.5gbase-t'),
+    (1130, '5gbase-t'),
+    (1150, '10gbase-t'),
+    (1170, '10gbase-cx4'),
+    (1200, '10gbase-x-sfpp'),
+    (1300, '10gbase-x-xfp'),
+    (1310, '10gbase-x-xenpak'),
+    (1320, '10gbase-x-x2'),
+    (1350, '25gbase-x-sfp28'),
+    (1400, '40gbase-x-qsfpp'),
+    (1420, '50gbase-x-sfp28'),
+    (1500, '100gbase-x-cfp'),
+    (1510, '100gbase-x-cfp2'),
+    (1520, '100gbase-x-cfp4'),
+    (1550, '100gbase-x-cpak'),
+    (1600, '100gbase-x-qsfp28'),
+    (1650, '200gbase-x-cfp2'),
+    (1700, '200gbase-x-qsfp56'),
+    (1750, '400gbase-x-qsfpdd'),
+    (1800, '400gbase-x-osfp'),
+    (2600, 'ieee802.11a'),
+    (2610, 'ieee802.11g'),
+    (2620, 'ieee802.11n'),
+    (2630, 'ieee802.11ac'),
+    (2640, 'ieee802.11ad'),
+    (2810, 'gsm'),
+    (2820, 'cdma'),
+    (2830, 'lte'),
+    (6100, 'sonet-oc3'),
+    (6200, 'sonet-oc12'),
+    (6300, 'sonet-oc48'),
+    (6400, 'sonet-oc192'),
+    (6500, 'sonet-oc768'),
+    (6600, 'sonet-oc1920'),
+    (6700, 'sonet-oc3840'),
+    (3010, '1gfc-sfp'),
+    (3020, '2gfc-sfp'),
+    (3040, '4gfc-sfp'),
+    (3080, '8gfc-sfpp'),
+    (3160, '16gfc-sfpp'),
+    (3320, '32gfc-sfp28'),
+    (3400, '128gfc-sfp28'),
+    (7010, 'inifiband-sdr'),
+    (7020, 'inifiband-ddr'),
+    (7030, 'inifiband-qdr'),
+    (7040, 'inifiband-fdr10'),
+    (7050, 'inifiband-fdr'),
+    (7060, 'inifiband-edr'),
+    (7070, 'inifiband-hdr'),
+    (7080, 'inifiband-ndr'),
+    (7090, 'inifiband-xdr'),
+    (4000, 't1'),
+    (4010, 'e1'),
+    (4040, 't3'),
+    (4050, 'e3'),
+    (5000, 'cisco-stackwise'),
+    (5050, 'cisco-stackwise-plus'),
+    (5100, 'cisco-flexstack'),
+    (5150, 'cisco-flexstack-plus'),
+    (5200, 'juniper-vcp'),
+    (5300, 'extreme-summitstack'),
+    (5310, 'extreme-summitstack-128'),
+    (5320, 'extreme-summitstack-256'),
+    (5330, 'extreme-summitstack-512'),
+)
+
+
+INTERFACE_MODE_CHOICES = (
+    (100, 'access'),
+    (200, 'tagged'),
+    (300, 'tagged-all'),
+)
+
+
+def interfacetemplate_type_to_slug(apps, schema_editor):
+    InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
+    for id, slug in INTERFACE_TYPE_CHOICES:
+        InterfaceTemplate.objects.filter(type=id).update(type=slug)
+
+
+def interface_type_to_slug(apps, schema_editor):
+    Interface = apps.get_model('dcim', 'Interface')
+    for id, slug in INTERFACE_TYPE_CHOICES:
+        Interface.objects.filter(type=id).update(type=slug)
+
+
+def interface_mode_to_slug(apps, schema_editor):
+    Interface = apps.get_model('dcim', 'Interface')
+    for id, slug in INTERFACE_MODE_CHOICES:
+        Interface.objects.filter(mode=id).update(mode=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0081_3569_device_fields'),
+    ]
+
+    operations = [
+
+        # InterfaceTemplate.type
+        migrations.AlterField(
+            model_name='interfacetemplate',
+            name='type',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=interfacetemplate_type_to_slug
+        ),
+
+        # Interface.type
+        migrations.AlterField(
+            model_name='interface',
+            name='type',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=interface_type_to_slug
+        ),
+
+        # Interface.mode
+        migrations.AlterField(
+            model_name='interface',
+            name='mode',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=interface_mode_to_slug
+        ),
+        migrations.AlterField(
+            model_name='interface',
+            name='mode',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+    ]

+ 93 - 0
netbox/dcim/migrations/0082_3569_port_fields.py

@@ -0,0 +1,93 @@
+from django.db import migrations, models
+
+
+PORT_TYPE_CHOICES = (
+    (1000, '8p8c'),
+    (1100, '110-punch'),
+    (1200, 'bnc'),
+    (2000, 'st'),
+    (2100, 'sc'),
+    (2110, 'sc-apc'),
+    (2200, 'fc'),
+    (2300, 'lc'),
+    (2310, 'lc-apc'),
+    (2400, 'mtrj'),
+    (2500, 'mpo'),
+    (2600, 'lsh'),
+    (2610, 'lsh-apc'),
+)
+
+
+def frontporttemplate_type_to_slug(apps, schema_editor):
+    FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
+    for id, slug in PORT_TYPE_CHOICES:
+        FrontPortTemplate.objects.filter(type=id).update(type=slug)
+
+
+def rearporttemplate_type_to_slug(apps, schema_editor):
+    RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate')
+    for id, slug in PORT_TYPE_CHOICES:
+        RearPortTemplate.objects.filter(type=id).update(type=slug)
+
+
+def frontport_type_to_slug(apps, schema_editor):
+    FrontPort = apps.get_model('dcim', 'FrontPort')
+    for id, slug in PORT_TYPE_CHOICES:
+        FrontPort.objects.filter(type=id).update(type=slug)
+
+
+def rearport_type_to_slug(apps, schema_editor):
+    RearPort = apps.get_model('dcim', 'RearPort')
+    for id, slug in PORT_TYPE_CHOICES:
+        RearPort.objects.filter(type=id).update(type=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0082_3569_interface_fields'),
+    ]
+
+    operations = [
+
+        # FrontPortTemplate.type
+        migrations.AlterField(
+            model_name='frontporttemplate',
+            name='type',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=frontporttemplate_type_to_slug
+        ),
+
+        # RearPortTemplate.type
+        migrations.AlterField(
+            model_name='rearporttemplate',
+            name='type',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=rearporttemplate_type_to_slug
+        ),
+
+        # FrontPort.type
+        migrations.AlterField(
+            model_name='frontport',
+            name='type',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=frontport_type_to_slug
+        ),
+
+        # RearPort.type
+        migrations.AlterField(
+            model_name='rearport',
+            name='type',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=rearport_type_to_slug
+        ),
+    ]

+ 85 - 0
netbox/dcim/migrations/0083_3569_cable_fields.py

@@ -0,0 +1,85 @@
+from django.db import migrations, models
+
+
+CABLE_TYPE_CHOICES = (
+    (1300, 'cat3'),
+    (1500, 'cat5'),
+    (1510, 'cat5e'),
+    (1600, 'cat6'),
+    (1610, 'cat6a'),
+    (1700, 'cat7'),
+    (1800, 'dac-active'),
+    (1810, 'dac-passive'),
+    (1900, 'coaxial'),
+    (3000, 'mmf'),
+    (3010, 'mmf-om1'),
+    (3020, 'mmf-om2'),
+    (3030, 'mmf-om3'),
+    (3040, 'mmf-om4'),
+    (3500, 'smf'),
+    (3510, 'smf-os1'),
+    (3520, 'smf-os2'),
+    (3800, 'aoc'),
+    (5000, 'power'),
+)
+
+CABLE_LENGTH_UNIT_CHOICES = (
+    (1200, 'm'),
+    (1100, 'cm'),
+    (2100, 'ft'),
+    (2000, 'in'),
+)
+
+
+def cable_type_to_slug(apps, schema_editor):
+    Cable = apps.get_model('dcim', 'Cable')
+    for id, slug in CABLE_TYPE_CHOICES:
+        Cable.objects.filter(type=id).update(type=slug)
+
+
+def cable_length_unit_to_slug(apps, schema_editor):
+    Cable = apps.get_model('dcim', 'Cable')
+    for id, slug in CABLE_LENGTH_UNIT_CHOICES:
+        Cable.objects.filter(length_unit=id).update(length_unit=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0082_3569_port_fields'),
+    ]
+
+    operations = [
+
+        # Cable.type
+        migrations.AlterField(
+            model_name='cable',
+            name='type',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=cable_type_to_slug
+        ),
+        migrations.AlterField(
+            model_name='cable',
+            name='type',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+        # Cable.length_unit
+        migrations.AlterField(
+            model_name='cable',
+            name='length_unit',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=cable_length_unit_to_slug
+        ),
+        migrations.AlterField(
+            model_name='cable',
+            name='length_unit',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+    ]

+ 100 - 0
netbox/dcim/migrations/0084_3569_powerfeed_fields.py

@@ -0,0 +1,100 @@
+from django.db import migrations, models
+
+
+POWERFEED_STATUS_CHOICES = (
+    (0, 'offline'),
+    (1, 'active'),
+    (2, 'planned'),
+    (4, 'failed'),
+)
+
+POWERFEED_TYPE_CHOICES = (
+    (1, 'primary'),
+    (2, 'redundant'),
+)
+
+POWERFEED_SUPPLY_CHOICES = (
+    (1, 'ac'),
+    (2, 'dc'),
+)
+
+POWERFEED_PHASE_CHOICES = (
+    (1, 'single-phase'),
+    (3, 'three-phase'),
+)
+
+
+def powerfeed_status_to_slug(apps, schema_editor):
+    PowerFeed = apps.get_model('dcim', 'PowerFeed')
+    for id, slug in POWERFEED_STATUS_CHOICES:
+        PowerFeed.objects.filter(status=id).update(status=slug)
+
+
+def powerfeed_type_to_slug(apps, schema_editor):
+    PowerFeed = apps.get_model('dcim', 'PowerFeed')
+    for id, slug in POWERFEED_TYPE_CHOICES:
+        PowerFeed.objects.filter(type=id).update(type=slug)
+
+
+def powerfeed_supply_to_slug(apps, schema_editor):
+    PowerFeed = apps.get_model('dcim', 'PowerFeed')
+    for id, slug in POWERFEED_SUPPLY_CHOICES:
+        PowerFeed.objects.filter(supply=id).update(supply=slug)
+
+
+def powerfeed_phase_to_slug(apps, schema_editor):
+    PowerFeed = apps.get_model('dcim', 'PowerFeed')
+    for id, slug in POWERFEED_PHASE_CHOICES:
+        PowerFeed.objects.filter(phase=id).update(phase=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0083_3569_cable_fields'),
+    ]
+
+    operations = [
+
+        # PowerFeed.status
+        migrations.AlterField(
+            model_name='powerfeed',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=powerfeed_status_to_slug
+        ),
+
+        # PowerFeed.type
+        migrations.AlterField(
+            model_name='powerfeed',
+            name='type',
+            field=models.CharField(default='primary', max_length=50),
+        ),
+        migrations.RunPython(
+            code=powerfeed_type_to_slug
+        ),
+
+        # PowerFeed.supply
+        migrations.AlterField(
+            model_name='powerfeed',
+            name='supply',
+            field=models.CharField(default='ac', max_length=50),
+        ),
+        migrations.RunPython(
+            code=powerfeed_supply_to_slug
+        ),
+
+        # PowerFeed.phase
+        migrations.AlterField(
+            model_name='powerfeed',
+            name='phase',
+            field=models.CharField(default='single-phase', max_length=50),
+        ),
+        migrations.RunPython(
+            code=powerfeed_phase_to_slug
+        ),
+
+    ]

+ 62 - 0
netbox/dcim/migrations/0085_3569_poweroutlet_fields.py

@@ -0,0 +1,62 @@
+from django.db import migrations, models
+
+
+POWEROUTLET_FEED_LEG_CHOICES_CHOICES = (
+    (1, 'A'),
+    (2, 'B'),
+    (3, 'C'),
+)
+
+
+def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor):
+    PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate')
+    for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
+        PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug)
+
+
+def poweroutlet_feed_leg_to_slug(apps, schema_editor):
+    PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
+    for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
+        PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('dcim', '0084_3569_powerfeed_fields'),
+    ]
+
+    operations = [
+
+        # PowerOutletTemplate.feed_leg
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='feed_leg',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=poweroutlettemplate_feed_leg_to_slug
+        ),
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='feed_leg',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+        # PowerOutlet.feed_leg
+        migrations.AlterField(
+            model_name='poweroutlet',
+            name='feed_leg',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=poweroutlet_feed_leg_to_slug
+        ),
+        migrations.AlterField(
+            model_name='poweroutlet',
+            name='feed_leg',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+    ]

+ 151 - 102
netbox/dcim/models.py

@@ -245,9 +245,10 @@ class Site(ChangeLoggedModel, CustomFieldModel):
     slug = models.SlugField(
     slug = models.SlugField(
         unique=True
         unique=True
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=SITE_STATUS_CHOICES,
-        default=SITE_STATUS_ACTIVE
+    status = models.CharField(
+        max_length=50,
+        choices=SiteStatusChoices,
+        default=SiteStatusChoices.STATUS_ACTIVE
     )
     )
     region = models.ForeignKey(
     region = models.ForeignKey(
         to='dcim.Region',
         to='dcim.Region',
@@ -331,6 +332,12 @@ class Site(ChangeLoggedModel, CustomFieldModel):
         'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
         'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        SiteStatusChoices.STATUS_ACTIVE: 'success',
+        SiteStatusChoices.STATUS_PLANNED: 'info',
+        SiteStatusChoices.STATUS_RETIRED: 'danger',
+    }
+
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
@@ -362,7 +369,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
         )
         )
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
 
 
 #
 #
@@ -473,9 +480,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=RACK_STATUS_CHOICES,
-        default=RACK_STATUS_ACTIVE
+    status = models.CharField(
+        max_length=50,
+        choices=RackStatusChoices,
+        default=RackStatusChoices.STATUS_ACTIVE
     )
     )
     role = models.ForeignKey(
     role = models.ForeignKey(
         to='dcim.RackRole',
         to='dcim.RackRole',
@@ -497,15 +505,15 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         verbose_name='Asset tag',
         verbose_name='Asset tag',
         help_text='A unique tag used to identify this rack'
         help_text='A unique tag used to identify this rack'
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=RACK_TYPE_CHOICES,
+    type = models.CharField(
+        choices=RackTypeChoices,
+        max_length=50,
         blank=True,
         blank=True,
-        null=True,
         verbose_name='Type'
         verbose_name='Type'
     )
     )
     width = models.PositiveSmallIntegerField(
     width = models.PositiveSmallIntegerField(
-        choices=RACK_WIDTH_CHOICES,
-        default=RACK_WIDTH_19IN,
+        choices=RackWidthChoices,
+        default=RackWidthChoices.WIDTH_19IN,
         verbose_name='Width',
         verbose_name='Width',
         help_text='Rail-to-rail width'
         help_text='Rail-to-rail width'
     )
     )
@@ -527,10 +535,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    outer_unit = models.PositiveSmallIntegerField(
-        choices=RACK_DIMENSION_UNIT_CHOICES,
+    outer_unit = models.CharField(
+        max_length=50,
+        choices=RackDimensionUnitChoices,
         blank=True,
         blank=True,
-        null=True
     )
     )
     comments = models.TextField(
     comments = models.TextField(
         blank=True
         blank=True
@@ -552,6 +560,14 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
         'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        RackStatusChoices.STATUS_RESERVED: 'warning',
+        RackStatusChoices.STATUS_AVAILABLE: 'success',
+        RackStatusChoices.STATUS_PLANNED: 'info',
+        RackStatusChoices.STATUS_ACTIVE: 'primary',
+        RackStatusChoices.STATUS_DEPRECATED: 'danger',
+    }
+
     class Meta:
     class Meta:
         ordering = ['site', 'group', 'name']
         ordering = ['site', 'group', 'name']
         unique_together = [
         unique_together = [
@@ -568,10 +584,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
     def clean(self):
     def clean(self):
 
 
         # Validate outer dimensions and unit
         # Validate outer dimensions and unit
-        if (self.outer_width is not None or self.outer_depth is not None) and self.outer_unit is None:
+        if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
             raise ValidationError("Must specify a unit when setting an outer width/depth")
             raise ValidationError("Must specify a unit when setting an outer width/depth")
         elif self.outer_width is None and self.outer_depth is None:
         elif self.outer_width is None and self.outer_depth is None:
-            self.outer_unit = None
+            self.outer_unit = ''
 
 
         if self.pk:
         if self.pk:
             # Validate that Rack is tall enough to house the installed Devices
             # Validate that Rack is tall enough to house the installed Devices
@@ -644,9 +660,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         return ""
         return ""
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
-    def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
+    def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, remove_redundant=False):
         """
         """
         Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
         Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
         Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
         Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
@@ -678,10 +694,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         return [u for u in elevation.values()]
         return [u for u in elevation.values()]
 
 
     def get_front_elevation(self):
     def get_front_elevation(self):
-        return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True)
+        return self.get_rack_units(face=DeviceFaceChoices.FACE_FRONT, remove_redundant=True)
 
 
     def get_rear_elevation(self):
     def get_rear_elevation(self):
-        return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True)
+        return self.get_rack_units(face=DeviceFaceChoices.FACE_REAR, remove_redundant=True)
 
 
     def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
     def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
         """
         """
@@ -910,12 +926,13 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         verbose_name='Is full depth',
         verbose_name='Is full depth',
         help_text='Device consumes both front and rear rack faces'
         help_text='Device consumes both front and rear rack faces'
     )
     )
-    subdevice_role = models.NullBooleanField(
-        default=None,
+    subdevice_role = models.CharField(
+        max_length=50,
+        choices=SubdeviceRoleChoices,
+        blank=True,
         verbose_name='Parent/child status',
         verbose_name='Parent/child status',
-        choices=SUBDEVICE_ROLE_CHOICES,
-        help_text='Parent devices house child devices in device bays. Select '
-                  '"None" if this device type is neither a parent nor a child.'
+        help_text='Parent devices house child devices in device bays. Leave blank '
+                  'if this device type is neither a parent nor a child.'
     )
     )
     comments = models.TextField(
     comments = models.TextField(
         blank=True
         blank=True
@@ -959,7 +976,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
             self.part_number,
             self.part_number,
             self.u_height,
             self.u_height,
             self.is_full_depth,
             self.is_full_depth,
-            self.get_subdevice_role_display() if self.subdevice_role else None,
+            self.get_subdevice_role_display(),
             self.comments,
             self.comments,
         )
         )
 
 
@@ -979,13 +996,15 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                                     "{}U".format(d, d.rack, self.u_height)
                                     "{}U".format(d, d.rack, self.u_height)
                     })
                     })
 
 
-        if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count():
+        if (
+                self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
+        ) and self.device_bay_templates.count():
             raise ValidationError({
             raise ValidationError({
                 'subdevice_role': "Must delete all device bay templates associated with this device before "
                 'subdevice_role': "Must delete all device bay templates associated with this device before "
                                   "declassifying it as a parent device."
                                   "declassifying it as a parent device."
             })
             })
 
 
-        if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD:
+        if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD:
             raise ValidationError({
             raise ValidationError({
                 'u_height': "Child device types must be 0U."
                 'u_height': "Child device types must be 0U."
             })
             })
@@ -996,11 +1015,11 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
 
 
     @property
     @property
     def is_parent_device(self):
     def is_parent_device(self):
-        return bool(self.subdevice_role)
+        return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT
 
 
     @property
     @property
     def is_child_device(self):
     def is_child_device(self):
-        return bool(self.subdevice_role is False)
+        return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
 
 
 
 
 class ConsolePortTemplate(ComponentTemplateModel):
 class ConsolePortTemplate(ComponentTemplateModel):
@@ -1017,7 +1036,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         blank=True
         blank=True
     )
     )
 
 
@@ -1052,7 +1071,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         blank=True
         blank=True
     )
     )
 
 
@@ -1087,7 +1106,7 @@ class PowerPortTemplate(ComponentTemplateModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=PowerPortTypes.CHOICES,
+        choices=PowerPortTypeChoices,
         blank=True
         blank=True
     )
     )
     maximum_draw = models.PositiveSmallIntegerField(
     maximum_draw = models.PositiveSmallIntegerField(
@@ -1135,7 +1154,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=PowerOutletTypes.CHOICES,
+        choices=PowerOutletTypeChoices,
         blank=True
         blank=True
     )
     )
     power_port = models.ForeignKey(
     power_port = models.ForeignKey(
@@ -1145,10 +1164,10 @@ class PowerOutletTemplate(ComponentTemplateModel):
         null=True,
         null=True,
         related_name='poweroutlet_templates'
         related_name='poweroutlet_templates'
     )
     )
-    feed_leg = models.PositiveSmallIntegerField(
-        choices=POWERFEED_LEG_CHOICES,
+    feed_leg = models.CharField(
+        max_length=50,
+        choices=PowerOutletFeedLegChoices,
         blank=True,
         blank=True,
-        null=True,
         help_text="Phase (for three-phase feeds)"
         help_text="Phase (for three-phase feeds)"
     )
     )
 
 
@@ -1194,9 +1213,9 @@ class InterfaceTemplate(ComponentTemplateModel):
     name = models.CharField(
     name = models.CharField(
         max_length=64
         max_length=64
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=IFACE_TYPE_CHOICES,
-        default=IFACE_TYPE_10GE_SFP_PLUS
+    type = models.CharField(
+        max_length=50,
+        choices=InterfaceTypeChoices
     )
     )
     mgmt_only = models.BooleanField(
     mgmt_only = models.BooleanField(
         default=False,
         default=False,
@@ -1233,8 +1252,9 @@ class FrontPortTemplate(ComponentTemplateModel):
     name = models.CharField(
     name = models.CharField(
         max_length=64
         max_length=64
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=PORT_TYPE_CHOICES
+    type = models.CharField(
+        max_length=50,
+        choices=PortTypeChoices
     )
     )
     rear_port = models.ForeignKey(
     rear_port = models.ForeignKey(
         to='dcim.RearPortTemplate',
         to='dcim.RearPortTemplate',
@@ -1300,8 +1320,9 @@ class RearPortTemplate(ComponentTemplateModel):
     name = models.CharField(
     name = models.CharField(
         max_length=64
         max_length=64
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=PORT_TYPE_CHOICES
+    type = models.CharField(
+        max_length=50,
+        choices=PortTypeChoices
     )
     )
     positions = models.PositiveSmallIntegerField(
     positions = models.PositiveSmallIntegerField(
         default=1,
         default=1,
@@ -1526,16 +1547,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         verbose_name='Position (U)',
         verbose_name='Position (U)',
         help_text='The lowest-numbered unit occupied by the device'
         help_text='The lowest-numbered unit occupied by the device'
     )
     )
-    face = models.PositiveSmallIntegerField(
+    face = models.CharField(
+        max_length=50,
         blank=True,
         blank=True,
-        null=True,
-        choices=RACK_FACE_CHOICES,
+        choices=DeviceFaceChoices,
         verbose_name='Rack face'
         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(
     primary_ip4 = models.OneToOneField(
         to='ipam.IPAddress',
         to='ipam.IPAddress',
@@ -1597,6 +1618,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
         '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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
         unique_together = [
         unique_together = [
@@ -1625,7 +1656,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
             })
             })
 
 
         if self.rack is None:
         if self.rack is None:
-            if self.face is not None:
+            if self.face:
                 raise ValidationError({
                 raise ValidationError({
                     'face': "Cannot select a rack face without assigning a rack.",
                     'face': "Cannot select a rack face without assigning a rack.",
                 })
                 })
@@ -1635,7 +1666,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
                 })
                 })
 
 
         # Validate position/face combination
         # Validate position/face combination
-        if self.position and self.face is None:
+        if self.position and not self.face:
             raise ValidationError({
             raise ValidationError({
                 'face': "Must specify rack face when defining rack position.",
                 'face': "Must specify rack face when defining rack position.",
             })
             })
@@ -1850,7 +1881,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         return Device.objects.filter(parent_bay__device=self.pk)
         return Device.objects.filter(parent_bay__device=self.pk)
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
 
 
 #
 #
@@ -1871,7 +1902,7 @@ class ConsolePort(CableTermination, ComponentModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         blank=True
         blank=True
     )
     )
     connected_endpoint = models.OneToOneField(
     connected_endpoint = models.OneToOneField(
@@ -1928,7 +1959,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=ConsolePortTypes.CHOICES,
+        choices=ConsolePortTypeChoices,
         blank=True
         blank=True
     )
     )
     connection_status = models.NullBooleanField(
     connection_status = models.NullBooleanField(
@@ -1977,7 +2008,7 @@ class PowerPort(CableTermination, ComponentModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=PowerPortTypes.CHOICES,
+        choices=PowerPortTypeChoices,
         blank=True
         blank=True
     )
     )
     maximum_draw = models.PositiveSmallIntegerField(
     maximum_draw = models.PositiveSmallIntegerField(
@@ -2077,8 +2108,8 @@ class PowerPort(CableTermination, ComponentModel):
             }
             }
 
 
             # Calculate per-leg aggregates for three-phase feeds
             # Calculate per-leg aggregates for three-phase feeds
-            if self._connected_powerfeed and self._connected_powerfeed.phase == POWERFEED_PHASE_3PHASE:
-                for leg, leg_name in POWERFEED_LEG_CHOICES:
+            if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
+                for leg, leg_name in PowerOutletFeedLegChoices:
                     outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
                     outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
                     utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
                     utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
                         maximum_draw_total=Sum('maximum_draw'),
                         maximum_draw_total=Sum('maximum_draw'),
@@ -2120,7 +2151,7 @@ class PowerOutlet(CableTermination, ComponentModel):
     )
     )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
-        choices=PowerOutletTypes.CHOICES,
+        choices=PowerOutletTypeChoices,
         blank=True
         blank=True
     )
     )
     power_port = models.ForeignKey(
     power_port = models.ForeignKey(
@@ -2130,10 +2161,10 @@ class PowerOutlet(CableTermination, ComponentModel):
         null=True,
         null=True,
         related_name='poweroutlets'
         related_name='poweroutlets'
     )
     )
-    feed_leg = models.PositiveSmallIntegerField(
-        choices=POWERFEED_LEG_CHOICES,
+    feed_leg = models.CharField(
+        max_length=50,
+        choices=PowerOutletFeedLegChoices,
         blank=True,
         blank=True,
-        null=True,
         help_text="Phase (for three-phase feeds)"
         help_text="Phase (for three-phase feeds)"
     )
     )
     connection_status = models.NullBooleanField(
     connection_status = models.NullBooleanField(
@@ -2226,9 +2257,9 @@ class Interface(CableTermination, ComponentModel):
         blank=True,
         blank=True,
         verbose_name='Parent LAG'
         verbose_name='Parent LAG'
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=IFACE_TYPE_CHOICES,
-        default=IFACE_TYPE_10GE_SFP_PLUS
+    type = models.CharField(
+        max_length=50,
+        choices=InterfaceTypeChoices
     )
     )
     enabled = models.BooleanField(
     enabled = models.BooleanField(
         default=True
         default=True
@@ -2249,10 +2280,10 @@ class Interface(CableTermination, ComponentModel):
         verbose_name='OOB Management',
         verbose_name='OOB Management',
         help_text='This interface is used only for out-of-band management'
         help_text='This interface is used only for out-of-band management'
     )
     )
-    mode = models.PositiveSmallIntegerField(
-        choices=IFACE_MODE_CHOICES,
+    mode = models.CharField(
+        max_length=50,
+        choices=InterfaceModeChoices,
         blank=True,
         blank=True,
-        null=True
     )
     )
     untagged_vlan = models.ForeignKey(
     untagged_vlan = models.ForeignKey(
         to='ipam.VLAN',
         to='ipam.VLAN',
@@ -2311,7 +2342,7 @@ class Interface(CableTermination, ComponentModel):
             raise ValidationError("An interface must belong to either a device or a virtual machine.")
             raise ValidationError("An interface must belong to either a device or a virtual machine.")
 
 
         # VM interfaces must be virtual
         # VM interfaces must be virtual
-        if self.virtual_machine and self.type is not IFACE_TYPE_VIRTUAL:
+        if self.virtual_machine and self.type is not InterfaceTypeChoices.TYPE_VIRTUAL:
             raise ValidationError({
             raise ValidationError({
                 'type': "Virtual machines can only have virtual interfaces."
                 'type': "Virtual machines can only have virtual interfaces."
             })
             })
@@ -2340,7 +2371,7 @@ class Interface(CableTermination, ComponentModel):
             })
             })
 
 
         # Only a LAG can have LAG members
         # Only a LAG can have LAG members
-        if self.type != IFACE_TYPE_LAG and self.member_interfaces.exists():
+        if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists():
             raise ValidationError({
             raise ValidationError({
                 'type': "Cannot change interface type; it has LAG members ({}).".format(
                 'type': "Cannot change interface type; it has LAG members ({}).".format(
                     ", ".join([iface.name for iface in self.member_interfaces.all()])
                     ", ".join([iface.name for iface in self.member_interfaces.all()])
@@ -2361,7 +2392,7 @@ class Interface(CableTermination, ComponentModel):
             self.untagged_vlan = None
             self.untagged_vlan = None
 
 
         # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
         # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
-        if self.pk and self.mode is not IFACE_MODE_TAGGED:
+        if self.pk and self.mode is not InterfaceModeChoices.MODE_TAGGED:
             self.tagged_vlans.clear()
             self.tagged_vlans.clear()
 
 
         return super().save(*args, **kwargs)
         return super().save(*args, **kwargs)
@@ -2423,7 +2454,7 @@ class Interface(CableTermination, ComponentModel):
 
 
     @property
     @property
     def is_lag(self):
     def is_lag(self):
-        return self.type == IFACE_TYPE_LAG
+        return self.type == InterfaceTypeChoices.TYPE_LAG
 
 
     @property
     @property
     def count_ipaddresses(self):
     def count_ipaddresses(self):
@@ -2446,8 +2477,9 @@ class FrontPort(CableTermination, ComponentModel):
     name = models.CharField(
     name = models.CharField(
         max_length=64
         max_length=64
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=PORT_TYPE_CHOICES
+    type = models.CharField(
+        max_length=50,
+        choices=PortTypeChoices
     )
     )
     rear_port = models.ForeignKey(
     rear_port = models.ForeignKey(
         to='dcim.RearPort',
         to='dcim.RearPort',
@@ -2513,8 +2545,9 @@ class RearPort(CableTermination, ComponentModel):
     name = models.CharField(
     name = models.CharField(
         max_length=64
         max_length=64
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=PORT_TYPE_CHOICES
+    type = models.CharField(
+        max_length=50,
+        choices=PortTypeChoices
     )
     )
     positions = models.PositiveSmallIntegerField(
     positions = models.PositiveSmallIntegerField(
         default=1,
         default=1,
@@ -2776,10 +2809,10 @@ class Cable(ChangeLoggedModel):
         ct_field='termination_b_type',
         ct_field='termination_b_type',
         fk_field='termination_b_id'
         fk_field='termination_b_id'
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=CABLE_TYPE_CHOICES,
-        blank=True,
-        null=True
+    type = models.CharField(
+        max_length=50,
+        choices=CableTypeChoices,
+        blank=True
     )
     )
     status = models.BooleanField(
     status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
@@ -2796,10 +2829,10 @@ class Cable(ChangeLoggedModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    length_unit = models.PositiveSmallIntegerField(
-        choices=CABLE_LENGTH_UNIT_CHOICES,
+    length_unit = models.CharField(
+        max_length=50,
+        choices=CableLengthUnitChoices,
         blank=True,
         blank=True,
-        null=True
     )
     )
     # Stores the normalized length (in meters) for database ordering
     # Stores the normalized length (in meters) for database ordering
     _abs_length = models.DecimalField(
     _abs_length = models.DecimalField(
@@ -2927,10 +2960,10 @@ class Cable(ChangeLoggedModel):
             ))
             ))
 
 
         # Validate length and length_unit
         # Validate length and length_unit
-        if self.length is not None and self.length_unit is None:
+        if self.length is not None and not self.length_unit:
             raise ValidationError("Must specify a unit when setting a cable length")
             raise ValidationError("Must specify a unit when setting a cable length")
         elif self.length is None:
         elif self.length is None:
-            self.length_unit = None
+            self.length_unit = ''
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
 
 
@@ -3074,21 +3107,25 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
     name = models.CharField(
     name = models.CharField(
         max_length=50
         max_length=50
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=POWERFEED_STATUS_CHOICES,
-        default=POWERFEED_STATUS_ACTIVE
+    status = models.CharField(
+        max_length=50,
+        choices=PowerFeedStatusChoices,
+        default=PowerFeedStatusChoices.STATUS_ACTIVE
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=POWERFEED_TYPE_CHOICES,
-        default=POWERFEED_TYPE_PRIMARY
+    type = models.CharField(
+        max_length=50,
+        choices=PowerFeedTypeChoices,
+        default=PowerFeedTypeChoices.TYPE_PRIMARY
     )
     )
-    supply = models.PositiveSmallIntegerField(
-        choices=POWERFEED_SUPPLY_CHOICES,
-        default=POWERFEED_SUPPLY_AC
+    supply = models.CharField(
+        max_length=50,
+        choices=PowerFeedSupplyChoices,
+        default=PowerFeedSupplyChoices.SUPPLY_AC
     )
     )
-    phase = models.PositiveSmallIntegerField(
-        choices=POWERFEED_PHASE_CHOICES,
-        default=POWERFEED_PHASE_SINGLE
+    phase = models.CharField(
+        max_length=50,
+        choices=PowerFeedPhaseChoices,
+        default=PowerFeedPhaseChoices.PHASE_SINGLE
     )
     )
     voltage = models.PositiveSmallIntegerField(
     voltage = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1)],
         validators=[MinValueValidator(1)],
@@ -3123,6 +3160,18 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
         'amperage', 'max_utilization', 'comments',
         'amperage', 'max_utilization', 'comments',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        PowerFeedStatusChoices.STATUS_OFFLINE: 'warning',
+        PowerFeedStatusChoices.STATUS_ACTIVE: 'success',
+        PowerFeedStatusChoices.STATUS_PLANNED: 'info',
+        PowerFeedStatusChoices.STATUS_FAILED: 'danger',
+    }
+
+    TYPE_CLASS_MAP = {
+        PowerFeedTypeChoices.TYPE_PRIMARY: 'success',
+        PowerFeedTypeChoices.TYPE_REDUNDANT: 'info',
+    }
+
     class Meta:
     class Meta:
         ordering = ['power_panel', 'name']
         ordering = ['power_panel', 'name']
         unique_together = ['power_panel', 'name']
         unique_together = ['power_panel', 'name']
@@ -3162,7 +3211,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
 
 
         # Cache the available_power property on the instance
         # Cache the available_power property on the instance
         kva = self.voltage * self.amperage * (self.max_utilization / 100)
         kva = self.voltage * self.amperage * (self.max_utilization / 100)
-        if self.phase == POWERFEED_PHASE_3PHASE:
+        if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
             self.available_power = round(kva * 1.732)
             self.available_power = round(kva * 1.732)
         else:
         else:
             self.available_power = round(kva)
             self.available_power = round(kva)
@@ -3170,7 +3219,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
         super().save(*args, **kwargs)
         super().save(*args, **kwargs)
 
 
     def get_type_class(self):
     def get_type_class(self):
-        return STATUS_CLASSES[self.type]
+        return self.TYPE_CLASS_MAP.get(self.type)
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)

+ 0 - 8
netbox/dcim/tables.py

@@ -156,10 +156,6 @@ DEVICE_PRIMARY_IP = """
 {{ record.primary_ip4.address.ip|default:"" }}
 {{ record.primary_ip4.address.ip|default:"" }}
 """
 """
 
 
-SUBDEVICE_ROLE_TEMPLATE = """
-{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
-"""
-
 DEVICETYPE_INSTANCES_TEMPLATE = """
 DEVICETYPE_INSTANCES_TEMPLATE = """
 <a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
 <a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
 """
 """
@@ -391,10 +387,6 @@ class DeviceTypeTable(BaseTable):
         verbose_name='Device Type'
         verbose_name='Device Type'
     )
     )
     is_full_depth = BooleanColumn(verbose_name='Full Depth')
     is_full_depth = BooleanColumn(verbose_name='Full Depth')
-    subdevice_role = tables.TemplateColumn(
-        template_code=SUBDEVICE_ROLE_TEMPLATE,
-        verbose_name='Subdevice Role'
-    )
     instance_count = tables.TemplateColumn(
     instance_count = tables.TemplateColumn(
         template_code=DEVICETYPE_INSTANCES_TEMPLATE,
         template_code=DEVICETYPE_INSTANCES_TEMPLATE,
         verbose_name='Instances'
         verbose_name='Instances'

+ 44 - 43
netbox/dcim/tests/test_api.py

@@ -3,6 +3,7 @@ from netaddr import IPNetwork
 from rest_framework import status
 from rest_framework import status
 
 
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
+from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import (
 from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -180,7 +181,7 @@ class SiteTest(APITestCase):
             'name': 'Test Site 4',
             'name': 'Test Site 4',
             'slug': 'test-site-4',
             'slug': 'test-site-4',
             'region': self.region1.pk,
             'region': self.region1.pk,
-            'status': SITE_STATUS_ACTIVE,
+            'status': SiteStatusChoices.STATUS_ACTIVE,
         }
         }
 
 
         url = reverse('dcim-api:site-list')
         url = reverse('dcim-api:site-list')
@@ -200,19 +201,19 @@ class SiteTest(APITestCase):
                 'name': 'Test Site 4',
                 'name': 'Test Site 4',
                 'slug': 'test-site-4',
                 'slug': 'test-site-4',
                 'region': self.region1.pk,
                 'region': self.region1.pk,
-                'status': SITE_STATUS_ACTIVE,
+                'status': SiteStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'name': 'Test Site 5',
                 'name': 'Test Site 5',
                 'slug': 'test-site-5',
                 'slug': 'test-site-5',
                 'region': self.region1.pk,
                 'region': self.region1.pk,
-                'status': SITE_STATUS_ACTIVE,
+                'status': SiteStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'name': 'Test Site 6',
                 'name': 'Test Site 6',
                 'slug': 'test-site-6',
                 'slug': 'test-site-6',
                 'region': self.region1.pk,
                 'region': self.region1.pk,
-                'status': SITE_STATUS_ACTIVE,
+                'status': SiteStatusChoices.STATUS_ACTIVE,
             },
             },
         ]
         ]
 
 
@@ -2473,7 +2474,7 @@ class InterfaceTest(APITestCase):
         data = {
         data = {
             'device': self.device.pk,
             'device': self.device.pk,
             'name': 'Test Interface 4',
             'name': 'Test Interface 4',
-            'mode': IFACE_MODE_TAGGED,
+            'mode': InterfaceModeChoices.MODE_TAGGED,
             'untagged_vlan': self.vlan3.id,
             'untagged_vlan': self.vlan3.id,
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
         }
         }
@@ -2520,21 +2521,21 @@ class InterfaceTest(APITestCase):
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 4',
                 'name': 'Test Interface 4',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 5',
                 'name': 'Test Interface 5',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 6',
                 'name': 'Test Interface 6',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
@@ -2553,7 +2554,7 @@ class InterfaceTest(APITestCase):
     def test_update_interface(self):
     def test_update_interface(self):
 
 
         lag_interface = Interface.objects.create(
         lag_interface = Interface.objects.create(
-            device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG
+            device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG
         )
         )
 
 
         data = {
         data = {
@@ -2590,11 +2591,11 @@ class DeviceBayTest(APITestCase):
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         self.devicetype1 = DeviceType.objects.create(
         self.devicetype1 = DeviceType.objects.create(
             manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
             manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
-            subdevice_role=SUBDEVICE_ROLE_PARENT
+            subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
         )
         )
         self.devicetype2 = DeviceType.objects.create(
         self.devicetype2 = DeviceType.objects.create(
             manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
             manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
-            subdevice_role=SUBDEVICE_ROLE_CHILD
+            subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
         )
         )
         devicerole = DeviceRole.objects.create(
         devicerole = DeviceRole.objects.create(
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -2841,7 +2842,7 @@ class CableTest(APITestCase):
         )
         )
         for device in [self.device1, self.device2]:
         for device in [self.device1, self.device2]:
             for i in range(0, 10):
             for i in range(0, 10):
-                Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
+                Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
 
 
         self.cable1 = Cable(
         self.cable1 = Cable(
             termination_a=self.device1.interfaces.get(name='eth0'),
             termination_a=self.device1.interfaces.get(name='eth0'),
@@ -3033,16 +3034,16 @@ class ConnectionTest(APITestCase):
             device=self.device2, name='Test Console Server Port 1'
             device=self.device2, name='Test Console Server Port 1'
         )
         )
         rearport1 = RearPort.objects.create(
         rearport1 = RearPort.objects.create(
-            device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport1 = FrontPort.objects.create(
         frontport1 = FrontPort.objects.create(
-            device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
+            device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
         )
         )
         rearport2 = RearPort.objects.create(
         rearport2 = RearPort.objects.create(
-            device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
+            device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport2 = FrontPort.objects.create(
         frontport2 = FrontPort.objects.create(
-            device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
+            device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
         )
         )
 
 
         url = reverse('dcim-api:cable-list')
         url = reverse('dcim-api:cable-list')
@@ -3161,16 +3162,16 @@ class ConnectionTest(APITestCase):
             device=self.device2, name='Test Interface 2'
             device=self.device2, name='Test Interface 2'
         )
         )
         rearport1 = RearPort.objects.create(
         rearport1 = RearPort.objects.create(
-            device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport1 = FrontPort.objects.create(
         frontport1 = FrontPort.objects.create(
-            device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
+            device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
         )
         )
         rearport2 = RearPort.objects.create(
         rearport2 = RearPort.objects.create(
-            device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
+            device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport2 = FrontPort.objects.create(
         frontport2 = FrontPort.objects.create(
-            device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
+            device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
         )
         )
 
 
         url = reverse('dcim-api:cable-list')
         url = reverse('dcim-api:cable-list')
@@ -3272,16 +3273,16 @@ class ConnectionTest(APITestCase):
             circuit=circuit, term_side='A', site=self.site, port_speed=10000
             circuit=circuit, term_side='A', site=self.site, port_speed=10000
         )
         )
         rearport1 = RearPort.objects.create(
         rearport1 = RearPort.objects.create(
-            device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport1 = FrontPort.objects.create(
         frontport1 = FrontPort.objects.create(
-            device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
+            device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
         )
         )
         rearport2 = RearPort.objects.create(
         rearport2 = RearPort.objects.create(
-            device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
+            device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport2 = FrontPort.objects.create(
         frontport2 = FrontPort.objects.create(
-            device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
+            device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
         )
         )
 
 
         url = reverse('dcim-api:cable-list')
         url = reverse('dcim-api:cable-list')
@@ -3410,23 +3411,23 @@ class VirtualChassisTest(APITestCase):
             device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
             device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
         )
         )
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
+            Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
 
 
         # Create two VirtualChassis with three members each
         # Create two VirtualChassis with three members each
         self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
         self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
@@ -3678,22 +3679,22 @@ class PowerFeedTest(APITestCase):
             site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
             site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
         )
         )
         self.powerfeed1 = PowerFeed.objects.create(
         self.powerfeed1 = PowerFeed.objects.create(
-            power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
+            power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY
         )
         )
         self.powerfeed2 = PowerFeed.objects.create(
         self.powerfeed2 = PowerFeed.objects.create(
-            power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
+            power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
         )
         )
         self.powerfeed3 = PowerFeed.objects.create(
         self.powerfeed3 = PowerFeed.objects.create(
-            power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
+            power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY
         )
         )
         self.powerfeed4 = PowerFeed.objects.create(
         self.powerfeed4 = PowerFeed.objects.create(
-            power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
+            power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
         )
         )
         self.powerfeed5 = PowerFeed.objects.create(
         self.powerfeed5 = PowerFeed.objects.create(
-            power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
+            power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY
         )
         )
         self.powerfeed6 = PowerFeed.objects.create(
         self.powerfeed6 = PowerFeed.objects.create(
-            power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
+            power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
         )
         )
 
 
     def test_get_powerfeed(self):
     def test_get_powerfeed(self):
@@ -3726,7 +3727,7 @@ class PowerFeedTest(APITestCase):
             'name': 'Test Power Feed 4A',
             'name': 'Test Power Feed 4A',
             'power_panel': self.powerpanel1.pk,
             'power_panel': self.powerpanel1.pk,
             'rack': self.rack4.pk,
             'rack': self.rack4.pk,
-            'type': POWERFEED_TYPE_PRIMARY,
+            'type': PowerFeedTypeChoices.TYPE_PRIMARY,
         }
         }
 
 
         url = reverse('dcim-api:powerfeed-list')
         url = reverse('dcim-api:powerfeed-list')
@@ -3746,13 +3747,13 @@ class PowerFeedTest(APITestCase):
                 'name': 'Test Power Feed 4A',
                 'name': 'Test Power Feed 4A',
                 'power_panel': self.powerpanel1.pk,
                 'power_panel': self.powerpanel1.pk,
                 'rack': self.rack4.pk,
                 'rack': self.rack4.pk,
-                'type': POWERFEED_TYPE_PRIMARY,
+                'type': PowerFeedTypeChoices.TYPE_PRIMARY,
             },
             },
             {
             {
                 'name': 'Test Power Feed 4B',
                 'name': 'Test Power Feed 4B',
                 'power_panel': self.powerpanel1.pk,
                 'power_panel': self.powerpanel1.pk,
                 'rack': self.rack4.pk,
                 'rack': self.rack4.pk,
-                'type': POWERFEED_TYPE_REDUNDANT,
+                'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
             },
             },
         ]
         ]
 
 
@@ -3769,7 +3770,7 @@ class PowerFeedTest(APITestCase):
         data = {
         data = {
             'name': 'Test Power Feed X',
             'name': 'Test Power Feed X',
             'rack': self.rack4.pk,
             'rack': self.rack4.pk,
-            'type': POWERFEED_TYPE_REDUNDANT,
+            'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
         }
         }
 
 
         url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
         url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})

+ 8 - 8
netbox/dcim/tests/test_forms.py

@@ -21,10 +21,10 @@ class DeviceTestCase(TestCase):
             'device_type': get_id(DeviceType, 'qfx5100-48s'),
             'device_type': get_id(DeviceType, 'qfx5100-48s'),
             'site': get_id(Site, 'test1'),
             'site': get_id(Site, 'test1'),
             'rack': '1',
             'rack': '1',
-            'face': RACK_FACE_FRONT,
+            'face': DeviceFaceChoices.FACE_FRONT,
             'position': 41,
             'position': 41,
             'platform': get_id(Platform, 'juniper-junos'),
             '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.is_valid(), test.fields['position'].choices)
         self.assertTrue(test.save())
         self.assertTrue(test.save())
@@ -38,10 +38,10 @@ class DeviceTestCase(TestCase):
             'device_type': get_id(DeviceType, 'qfx5100-48s'),
             'device_type': get_id(DeviceType, 'qfx5100-48s'),
             'site': get_id(Site, 'test1'),
             'site': get_id(Site, 'test1'),
             'rack': '1',
             'rack': '1',
-            'face': RACK_FACE_FRONT,
+            'face': DeviceFaceChoices.FACE_FRONT,
             'position': 1,
             'position': 1,
             'platform': get_id(Platform, 'juniper-junos'),
             'platform': get_id(Platform, 'juniper-junos'),
-            'status': DEVICE_STATUS_ACTIVE,
+            'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
         self.assertFalse(test.is_valid())
         self.assertFalse(test.is_valid())
 
 
@@ -54,10 +54,10 @@ class DeviceTestCase(TestCase):
             'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
             'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
             'site': get_id(Site, 'test1'),
             'site': get_id(Site, 'test1'),
             'rack': '1',
             'rack': '1',
-            'face': None,
+            'face': '',
             'position': None,
             'position': None,
             'platform': None,
             'platform': None,
-            'status': DEVICE_STATUS_ACTIVE,
+            'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
         self.assertTrue(test.is_valid())
         self.assertTrue(test.is_valid())
         self.assertTrue(test.save())
         self.assertTrue(test.save())
@@ -71,10 +71,10 @@ class DeviceTestCase(TestCase):
             'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
             'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
             'site': get_id(Site, 'test1'),
             'site': get_id(Site, 'test1'),
             'rack': '1',
             'rack': '1',
-            'face': RACK_FACE_REAR,
+            'face': DeviceFaceChoices.FACE_REAR,
             'position': None,
             'position': None,
             'platform': None,
             'platform': None,
-            'status': DEVICE_STATUS_ACTIVE,
+            'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
         self.assertTrue(test.is_valid())
         self.assertTrue(test.is_valid())
         self.assertTrue(test.save())
         self.assertTrue(test.save())

+ 17 - 17
netbox/dcim/tests/test_models.py

@@ -87,7 +87,7 @@ class RackTestCase(TestCase):
             site=self.site1,
             site=self.site1,
             rack=rack1,
             rack=rack1,
             position=43,
             position=43,
-            face=RACK_FACE_FRONT,
+            face=DeviceFaceChoices.FACE_FRONT,
         )
         )
         device1.save()
         device1.save()
 
 
@@ -117,7 +117,7 @@ class RackTestCase(TestCase):
             site=self.site1,
             site=self.site1,
             rack=self.rack,
             rack=self.rack,
             position=10,
             position=10,
-            face=RACK_FACE_REAR,
+            face=DeviceFaceChoices.FACE_REAR,
         )
         )
         device1.save()
         device1.save()
 
 
@@ -146,7 +146,7 @@ class RackTestCase(TestCase):
             site=self.site1,
             site=self.site1,
             rack=self.rack,
             rack=self.rack,
             position=None,
             position=None,
-            face=None,
+            face='',
         )
         )
         self.assertTrue(pdu)
         self.assertTrue(pdu)
 
 
@@ -187,20 +187,20 @@ class DeviceTestCase(TestCase):
             device_type=self.device_type,
             device_type=self.device_type,
             name='Power Outlet 1',
             name='Power Outlet 1',
             power_port=ppt,
             power_port=ppt,
-            feed_leg=POWERFEED_LEG_A
+            feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
         ).save()
         ).save()
 
 
         InterfaceTemplate(
         InterfaceTemplate(
             device_type=self.device_type,
             device_type=self.device_type,
             name='Interface 1',
             name='Interface 1',
-            type=IFACE_TYPE_1GE_FIXED,
+            type=InterfaceTypeChoices.TYPE_1GE_FIXED,
             mgmt_only=True
             mgmt_only=True
         ).save()
         ).save()
 
 
         rpt = RearPortTemplate(
         rpt = RearPortTemplate(
             device_type=self.device_type,
             device_type=self.device_type,
             name='Rear Port 1',
             name='Rear Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             positions=8
             positions=8
         )
         )
         rpt.save()
         rpt.save()
@@ -208,7 +208,7 @@ class DeviceTestCase(TestCase):
         FrontPortTemplate(
         FrontPortTemplate(
             device_type=self.device_type,
             device_type=self.device_type,
             name='Front Port 1',
             name='Front Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             rear_port=rpt,
             rear_port=rpt,
             rear_port_position=2
             rear_port_position=2
         ).save()
         ).save()
@@ -251,27 +251,27 @@ class DeviceTestCase(TestCase):
             device=d,
             device=d,
             name='Power Outlet 1',
             name='Power Outlet 1',
             power_port=pp,
             power_port=pp,
-            feed_leg=POWERFEED_LEG_A
+            feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
         )
         )
 
 
         Interface.objects.get(
         Interface.objects.get(
             device=d,
             device=d,
             name='Interface 1',
             name='Interface 1',
-            type=IFACE_TYPE_1GE_FIXED,
+            type=InterfaceTypeChoices.TYPE_1GE_FIXED,
             mgmt_only=True
             mgmt_only=True
         )
         )
 
 
         rp = RearPort.objects.get(
         rp = RearPort.objects.get(
             device=d,
             device=d,
             name='Rear Port 1',
             name='Rear Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             positions=8
             positions=8
         )
         )
 
 
         FrontPort.objects.get(
         FrontPort.objects.get(
             device=d,
             device=d,
             name='Front Port 1',
             name='Front Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             rear_port=rp,
             rear_port=rp,
             rear_port_position=2
             rear_port_position=2
         )
         )
@@ -379,7 +379,7 @@ class CableTestCase(TestCase):
         """
         """
         A cable cannot terminate to a virtual interface
         A cable cannot terminate to a virtual interface
         """
         """
-        virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL)
+        virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL)
         cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
         cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             cable.clean()
             cable.clean()
@@ -388,7 +388,7 @@ class CableTestCase(TestCase):
         """
         """
         A cable cannot terminate to a wireless interface
         A cable cannot terminate to a wireless interface
         """
         """
-        wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A)
+        wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A)
         cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
         cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             cable.clean()
             cable.clean()
@@ -421,16 +421,16 @@ class CablePathTestCase(TestCase):
             device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
             device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
         )
         )
         self.rear_port1 = RearPort.objects.create(
         self.rear_port1 = RearPort.objects.create(
-            device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         self.front_port1 = FrontPort.objects.create(
         self.front_port1 = FrontPort.objects.create(
-            device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1
+            device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1
         )
         )
         self.rear_port2 = RearPort.objects.create(
         self.rear_port2 = RearPort.objects.create(
-            device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C
+            device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C
         )
         )
         self.front_port2 = FrontPort.objects.create(
         self.front_port2 = FrontPort.objects.create(
-            device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2
+            device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2
         )
         )
 
 
     def test_path_completion(self):
     def test_path_completion(self):

+ 19 - 19
netbox/dcim/tests/test_views.py

@@ -255,15 +255,15 @@ power-outlets:
   - name: Power Outlet 1
   - name: Power Outlet 1
     type: iec-60320-c13
     type: iec-60320-c13
     power_port: Power Port 1
     power_port: Power Port 1
-    feed_leg: 1
+    feed_leg: A
   - name: Power Outlet 2
   - name: Power Outlet 2
     type: iec-60320-c13
     type: iec-60320-c13
     power_port: Power Port 1
     power_port: Power Port 1
-    feed_leg: 1
+    feed_leg: A
   - name: Power Outlet 3
   - name: Power Outlet 3
     type: iec-60320-c13
     type: iec-60320-c13
     power_port: Power Port 1
     power_port: Power Port 1
-    feed_leg: 1
+    feed_leg: A
 interfaces:
 interfaces:
   - name: Interface 1
   - name: Interface 1
     type: 1000base-t
     type: 1000base-t
@@ -326,29 +326,29 @@ device-bays:
         self.assertEqual(dt.consoleport_templates.count(), 3)
         self.assertEqual(dt.consoleport_templates.count(), 3)
         cp1 = ConsolePortTemplate.objects.first()
         cp1 = ConsolePortTemplate.objects.first()
         self.assertEqual(cp1.name, 'Console Port 1')
         self.assertEqual(cp1.name, 'Console Port 1')
-        self.assertEqual(cp1.type, ConsolePortTypes.TYPE_DE9)
+        self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
 
 
         self.assertEqual(dt.consoleserverport_templates.count(), 3)
         self.assertEqual(dt.consoleserverport_templates.count(), 3)
         csp1 = ConsoleServerPortTemplate.objects.first()
         csp1 = ConsoleServerPortTemplate.objects.first()
         self.assertEqual(csp1.name, 'Console Server Port 1')
         self.assertEqual(csp1.name, 'Console Server Port 1')
-        self.assertEqual(csp1.type, ConsolePortTypes.TYPE_RJ45)
+        self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
 
 
         self.assertEqual(dt.powerport_templates.count(), 3)
         self.assertEqual(dt.powerport_templates.count(), 3)
         pp1 = PowerPortTemplate.objects.first()
         pp1 = PowerPortTemplate.objects.first()
         self.assertEqual(pp1.name, 'Power Port 1')
         self.assertEqual(pp1.name, 'Power Port 1')
-        self.assertEqual(pp1.type, PowerPortTypes.TYPE_IEC_C14)
+        self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
 
 
         self.assertEqual(dt.poweroutlet_templates.count(), 3)
         self.assertEqual(dt.poweroutlet_templates.count(), 3)
         po1 = PowerOutletTemplate.objects.first()
         po1 = PowerOutletTemplate.objects.first()
         self.assertEqual(po1.name, 'Power Outlet 1')
         self.assertEqual(po1.name, 'Power Outlet 1')
-        self.assertEqual(po1.type, PowerOutletTypes.TYPE_IEC_C13)
+        self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
         self.assertEqual(po1.power_port, pp1)
         self.assertEqual(po1.power_port, pp1)
-        self.assertEqual(po1.feed_leg, POWERFEED_LEG_A)
+        self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
 
 
         self.assertEqual(dt.interface_templates.count(), 3)
         self.assertEqual(dt.interface_templates.count(), 3)
         iface1 = InterfaceTemplate.objects.first()
         iface1 = InterfaceTemplate.objects.first()
         self.assertEqual(iface1.name, 'Interface 1')
         self.assertEqual(iface1.name, 'Interface 1')
-        self.assertEqual(iface1.type, IFACE_TYPE_1GE_FIXED)
+        self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
         self.assertTrue(iface1.mgmt_only)
         self.assertTrue(iface1.mgmt_only)
 
 
         self.assertEqual(dt.rearport_templates.count(), 3)
         self.assertEqual(dt.rearport_templates.count(), 3)
@@ -514,28 +514,28 @@ class CableTestCase(TestCase):
         device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
         device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
         device2.save()
         device2.save()
 
 
-        iface1 = Interface(device=device1, name='Interface 1', type=IFACE_TYPE_1GE_FIXED)
+        iface1 = Interface(device=device1, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         iface1.save()
         iface1.save()
-        iface2 = Interface(device=device1, name='Interface 2', type=IFACE_TYPE_1GE_FIXED)
+        iface2 = Interface(device=device1, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         iface2.save()
         iface2.save()
-        iface3 = Interface(device=device1, name='Interface 3', type=IFACE_TYPE_1GE_FIXED)
+        iface3 = Interface(device=device1, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         iface3.save()
         iface3.save()
-        iface4 = Interface(device=device2, name='Interface 1', type=IFACE_TYPE_1GE_FIXED)
+        iface4 = Interface(device=device2, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         iface4.save()
         iface4.save()
-        iface5 = Interface(device=device2, name='Interface 2', type=IFACE_TYPE_1GE_FIXED)
+        iface5 = Interface(device=device2, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         iface5.save()
         iface5.save()
-        iface6 = Interface(device=device2, name='Interface 3', type=IFACE_TYPE_1GE_FIXED)
+        iface6 = Interface(device=device2, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
         iface6.save()
         iface6.save()
 
 
-        Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save()
-        Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save()
-        Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save()
+        Cable(termination_a=iface1, termination_b=iface4, type=CableTypeChoices.TYPE_CAT6).save()
+        Cable(termination_a=iface2, termination_b=iface5, type=CableTypeChoices.TYPE_CAT6).save()
+        Cable(termination_a=iface3, termination_b=iface6, type=CableTypeChoices.TYPE_CAT6).save()
 
 
     def test_cable_list(self):
     def test_cable_list(self):
 
 
         url = reverse('dcim:cable_list')
         url = reverse('dcim:cable_list')
         params = {
         params = {
-            "type": CABLE_TYPE_CAT6,
+            "type": CableTypeChoices.TYPE_CAT6,
         }
         }
 
 
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))

+ 6 - 6
netbox/extras/api/customfields.py

@@ -5,7 +5,7 @@ from django.db import transaction
 from rest_framework import serializers
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
 from rest_framework.exceptions import ValidationError
 
 
-from extras.constants import *
+from extras.choices import *
 from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
 from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
 from utilities.api import ValidatedModelSerializer
 from utilities.api import ValidatedModelSerializer
 
 
@@ -37,7 +37,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
             if value not in [None, '']:
             if value not in [None, '']:
 
 
                 # Validate integer
                 # Validate integer
-                if cf.type == CF_TYPE_INTEGER:
+                if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
                     try:
                     try:
                         int(value)
                         int(value)
                     except ValueError:
                     except ValueError:
@@ -46,13 +46,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
                         )
                         )
 
 
                 # Validate boolean
                 # Validate boolean
-                if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
+                if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
                     raise ValidationError(
                     raise ValidationError(
                         "Invalid value for boolean field {}: {}".format(field_name, value)
                         "Invalid value for boolean field {}: {}".format(field_name, value)
                     )
                     )
 
 
                 # Validate date
                 # Validate date
-                if cf.type == CF_TYPE_DATE:
+                if cf.type == CustomFieldTypeChoices.TYPE_DATE:
                     try:
                     try:
                         datetime.strptime(value, '%Y-%m-%d')
                         datetime.strptime(value, '%Y-%m-%d')
                     except ValueError:
                     except ValueError:
@@ -61,7 +61,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
                         )
                         )
 
 
                 # Validate selected choice
                 # Validate selected choice
-                if cf.type == CF_TYPE_SELECT:
+                if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
                     try:
                     try:
                         value = int(value)
                         value = int(value)
                     except ValueError:
                     except ValueError:
@@ -100,7 +100,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
             instance.custom_fields = {}
             instance.custom_fields = {}
             for field in fields:
             for field in fields:
                 value = instance.cf.get(field.name)
                 value = instance.cf.get(field.name)
-                if field.type == CF_TYPE_SELECT and value is not None:
+                if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
                     instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
                     instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
                 else:
                 else:
                     instance.custom_fields[field.name] = value
                     instance.custom_fields[field.name] = value

+ 4 - 3
netbox/extras/api/serializers.py

@@ -8,6 +8,7 @@ from dcim.api.nested_serializers import (
     NestedRegionSerializer, NestedSiteSerializer,
     NestedRegionSerializer, NestedSiteSerializer,
 )
 )
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
+from extras.choices import *
 from extras.constants import *
 from extras.constants import *
 from extras.models import (
 from extras.models import (
     ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
     ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
@@ -56,8 +57,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
 
 
 class ExportTemplateSerializer(ValidatedModelSerializer):
 class ExportTemplateSerializer(ValidatedModelSerializer):
     template_language = ChoiceField(
     template_language = ChoiceField(
-        choices=TEMPLATE_LANGUAGE_CHOICES,
-        default=TEMPLATE_LANGUAGE_JINJA2
+        choices=ExportTemplateLanguageChoices,
+        default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
     )
     )
 
 
     class Meta:
     class Meta:
@@ -255,7 +256,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
         read_only=True
         read_only=True
     )
     )
     action = ChoiceField(
     action = ChoiceField(
-        choices=OBJECTCHANGE_ACTION_CHOICES,
+        choices=ObjectChangeActionChoices,
         read_only=True
         read_only=True
     )
     )
     changed_object_type = ContentTypeField(
     changed_object_type = ContentTypeField(

+ 140 - 0
netbox/extras/choices.py

@@ -0,0 +1,140 @@
+from utilities.choices import ChoiceSet
+
+
+#
+# CustomFields
+#
+
+class CustomFieldTypeChoices(ChoiceSet):
+
+    TYPE_TEXT = 'text'
+    TYPE_INTEGER = 'integer'
+    TYPE_BOOLEAN = 'boolean'
+    TYPE_DATE = 'date'
+    TYPE_URL = 'url'
+    TYPE_SELECT = 'select'
+
+    CHOICES = (
+        (TYPE_TEXT, 'Text'),
+        (TYPE_INTEGER, 'Integer'),
+        (TYPE_BOOLEAN, 'Boolean (true/false)'),
+        (TYPE_DATE, 'Date'),
+        (TYPE_URL, 'URL'),
+        (TYPE_SELECT, 'Selection'),
+    )
+
+    LEGACY_MAP = {
+        TYPE_TEXT: 100,
+        TYPE_INTEGER: 200,
+        TYPE_BOOLEAN: 300,
+        TYPE_DATE: 400,
+        TYPE_URL: 500,
+        TYPE_SELECT: 600,
+    }
+
+
+class CustomFieldFilterLogicChoices(ChoiceSet):
+
+    FILTER_DISABLED = 'disabled'
+    FILTER_LOOSE = 'loose'
+    FILTER_EXACT = 'exact'
+
+    CHOICES = (
+        (FILTER_DISABLED, 'Disabled'),
+        (FILTER_LOOSE, 'Loose'),
+        (FILTER_EXACT, 'Exact'),
+    )
+
+    LEGACY_MAP = {
+        FILTER_DISABLED: 0,
+        FILTER_LOOSE: 1,
+        FILTER_EXACT: 2,
+    }
+
+
+#
+# CustomLinks
+#
+
+class CustomLinkButtonClassChoices(ChoiceSet):
+
+    CLASS_DEFAULT = 'default'
+    CLASS_PRIMARY = 'primary'
+    CLASS_SUCCESS = 'success'
+    CLASS_INFO = 'info'
+    CLASS_WARNING = 'warning'
+    CLASS_DANGER = 'danger'
+    CLASS_LINK = 'link'
+
+    CHOICES = (
+        (CLASS_DEFAULT, 'Default'),
+        (CLASS_PRIMARY, 'Primary (blue)'),
+        (CLASS_SUCCESS, 'Success (green)'),
+        (CLASS_INFO, 'Info (aqua)'),
+        (CLASS_WARNING, 'Warning (orange)'),
+        (CLASS_DANGER, 'Danger (red)'),
+        (CLASS_LINK, 'None (link)'),
+    )
+
+
+#
+# ObjectChanges
+#
+
+class ObjectChangeActionChoices(ChoiceSet):
+
+    ACTION_CREATE = 'create'
+    ACTION_UPDATE = 'update'
+    ACTION_DELETE = 'delete'
+
+    CHOICES = (
+        (ACTION_CREATE, 'Created'),
+        (ACTION_UPDATE, 'Updated'),
+        (ACTION_DELETE, 'Deleted'),
+    )
+
+    LEGACY_MAP = {
+        ACTION_CREATE: 1,
+        ACTION_UPDATE: 2,
+        ACTION_DELETE: 3,
+    }
+
+
+#
+# ExportTemplates
+#
+
+class ExportTemplateLanguageChoices(ChoiceSet):
+
+    LANGUAGE_DJANGO = 'django'
+    LANGUAGE_JINJA2 = 'jinja2'
+
+    CHOICES = (
+        (LANGUAGE_DJANGO, 'Django'),
+        (LANGUAGE_JINJA2, 'Jinja2'),
+    )
+
+    LEGACY_MAP = {
+        LANGUAGE_DJANGO: 10,
+        LANGUAGE_JINJA2: 20,
+    }
+
+
+#
+# Webhooks
+#
+
+class WebhookContentTypeChoices(ChoiceSet):
+
+    CONTENTTYPE_JSON = 'application/json'
+    CONTENTTYPE_FORMDATA = 'application/x-www-form-urlencoded'
+
+    CHOICES = (
+        (CONTENTTYPE_JSON, 'JSON'),
+        (CONTENTTYPE_FORMDATA, 'Form data'),
+    )
+
+    LEGACY_MAP = {
+        CONTENTTYPE_JSON: 1,
+        CONTENTTYPE_FORMDATA: 2,
+    }

+ 0 - 87
netbox/extras/constants.py

@@ -19,32 +19,6 @@ CUSTOMFIELD_MODELS = [
     'virtualization.virtualmachine',
     'virtualization.virtualmachine',
 ]
 ]
 
 
-# Custom field types
-CF_TYPE_TEXT = 100
-CF_TYPE_INTEGER = 200
-CF_TYPE_BOOLEAN = 300
-CF_TYPE_DATE = 400
-CF_TYPE_URL = 500
-CF_TYPE_SELECT = 600
-CUSTOMFIELD_TYPE_CHOICES = (
-    (CF_TYPE_TEXT, 'Text'),
-    (CF_TYPE_INTEGER, 'Integer'),
-    (CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
-    (CF_TYPE_DATE, 'Date'),
-    (CF_TYPE_URL, 'URL'),
-    (CF_TYPE_SELECT, 'Selection'),
-)
-
-# Custom field filter logic choices
-CF_FILTER_DISABLED = 0
-CF_FILTER_LOOSE = 1
-CF_FILTER_EXACT = 2
-CF_FILTER_CHOICES = (
-    (CF_FILTER_DISABLED, 'Disabled'),
-    (CF_FILTER_LOOSE, 'Loose'),
-    (CF_FILTER_EXACT, 'Exact'),
-)
-
 # Custom links
 # Custom links
 CUSTOMLINK_MODELS = [
 CUSTOMLINK_MODELS = [
     'circuits.circuit',
     'circuits.circuit',
@@ -68,23 +42,6 @@ CUSTOMLINK_MODELS = [
     'virtualization.virtualmachine',
     'virtualization.virtualmachine',
 ]
 ]
 
 
-BUTTON_CLASS_DEFAULT = 'default'
-BUTTON_CLASS_PRIMARY = 'primary'
-BUTTON_CLASS_SUCCESS = 'success'
-BUTTON_CLASS_INFO = 'info'
-BUTTON_CLASS_WARNING = 'warning'
-BUTTON_CLASS_DANGER = 'danger'
-BUTTON_CLASS_LINK = 'link'
-BUTTON_CLASS_CHOICES = (
-    (BUTTON_CLASS_DEFAULT, 'Default'),
-    (BUTTON_CLASS_PRIMARY, 'Primary (blue)'),
-    (BUTTON_CLASS_SUCCESS, 'Success (green)'),
-    (BUTTON_CLASS_INFO, 'Info (aqua)'),
-    (BUTTON_CLASS_WARNING, 'Warning (orange)'),
-    (BUTTON_CLASS_DANGER, 'Danger (red)'),
-    (BUTTON_CLASS_LINK, 'None (link)'),
-)
-
 # Graph types
 # Graph types
 GRAPH_TYPE_INTERFACE = 100
 GRAPH_TYPE_INTERFACE = 100
 GRAPH_TYPE_DEVICE = 150
 GRAPH_TYPE_DEVICE = 150
@@ -128,42 +85,6 @@ EXPORTTEMPLATE_MODELS = [
     'virtualization.virtualmachine',
     'virtualization.virtualmachine',
 ]
 ]
 
 
-# ExportTemplate language choices
-TEMPLATE_LANGUAGE_DJANGO = 10
-TEMPLATE_LANGUAGE_JINJA2 = 20
-TEMPLATE_LANGUAGE_CHOICES = (
-    (TEMPLATE_LANGUAGE_DJANGO, 'Django'),
-    (TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
-)
-
-# Change log actions
-OBJECTCHANGE_ACTION_CREATE = 1
-OBJECTCHANGE_ACTION_UPDATE = 2
-OBJECTCHANGE_ACTION_DELETE = 3
-OBJECTCHANGE_ACTION_CHOICES = (
-    (OBJECTCHANGE_ACTION_CREATE, 'Created'),
-    (OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
-    (OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
-)
-
-# User action types
-ACTION_CREATE = 1
-ACTION_IMPORT = 2
-ACTION_EDIT = 3
-ACTION_BULK_EDIT = 4
-ACTION_DELETE = 5
-ACTION_BULK_DELETE = 6
-ACTION_BULK_CREATE = 7
-ACTION_CHOICES = (
-    (ACTION_CREATE, 'created'),
-    (ACTION_BULK_CREATE, 'bulk created'),
-    (ACTION_IMPORT, 'imported'),
-    (ACTION_EDIT, 'modified'),
-    (ACTION_BULK_EDIT, 'bulk edited'),
-    (ACTION_DELETE, 'deleted'),
-    (ACTION_BULK_DELETE, 'bulk deleted'),
-)
-
 # Report logging levels
 # Report logging levels
 LOG_DEFAULT = 0
 LOG_DEFAULT = 0
 LOG_SUCCESS = 10
 LOG_SUCCESS = 10
@@ -178,14 +99,6 @@ LOG_LEVEL_CODES = {
     LOG_FAILURE: 'failure',
     LOG_FAILURE: 'failure',
 }
 }
 
 
-# webhook content types
-WEBHOOK_CT_JSON = 1
-WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
-WEBHOOK_CT_CHOICES = (
-    (WEBHOOK_CT_JSON, 'application/json'),
-    (WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
-)
-
 # Models which support registered webhooks
 # Models which support registered webhooks
 WEBHOOK_MODELS = [
 WEBHOOK_MODELS = [
     'circuits.circuit',
     'circuits.circuit',

+ 9 - 3
netbox/extras/filters.py

@@ -4,6 +4,7 @@ from django.db.models import Q
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
+from .choices import *
 from .constants import *
 from .constants import *
 from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
 from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
 
 
@@ -25,7 +26,7 @@ class CustomFieldFilter(django_filters.Filter):
             return queryset
             return queryset
 
 
         # Selection fields get special treatment (values must be integers)
         # Selection fields get special treatment (values must be integers)
-        if self.cf_type == CF_TYPE_SELECT:
+        if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
             try:
             try:
                 # Treat 0 as None
                 # Treat 0 as None
                 if int(value) == 0:
                 if int(value) == 0:
@@ -42,7 +43,8 @@ class CustomFieldFilter(django_filters.Filter):
                 return queryset.none()
                 return queryset.none()
 
 
         # Apply the assigned filter logic (exact or loose)
         # Apply the assigned filter logic (exact or loose)
-        if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
+        if (self.cf_type == CustomFieldTypeChoices.TYPE_BOOLEAN or
+                self.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT):
             queryset = queryset.filter(
             queryset = queryset.filter(
                 custom_field_values__field__name=self.field_name,
                 custom_field_values__field__name=self.field_name,
                 custom_field_values__serialized_value=value
                 custom_field_values__serialized_value=value
@@ -65,7 +67,11 @@ class CustomFieldFilterSet(django_filters.FilterSet):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         obj_type = ContentType.objects.get_for_model(self._meta.model)
         obj_type = ContentType.objects.get_for_model(self._meta.model)
-        custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
+        custom_fields = CustomField.objects.filter(
+            obj_type=obj_type
+        ).exclude(
+            filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
+        )
         for cf in custom_fields:
         for cf in custom_fields:
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
 
 

+ 8 - 7
netbox/extras/forms.py

@@ -13,6 +13,7 @@ from utilities.forms import (
     CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
     CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
     BOOLEAN_WITH_BLANK_CHOICES,
     BOOLEAN_WITH_BLANK_CHOICES,
 )
 )
+from .choices import *
 from .constants import *
 from .constants import *
 from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
 from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
 
 
@@ -28,18 +29,18 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
     field_dict = OrderedDict()
     field_dict = OrderedDict()
     custom_fields = CustomField.objects.filter(obj_type=content_type)
     custom_fields = CustomField.objects.filter(obj_type=content_type)
     if filterable_only:
     if filterable_only:
-        custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
+        custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
 
 
     for cf in custom_fields:
     for cf in custom_fields:
         field_name = 'cf_{}'.format(str(cf.name))
         field_name = 'cf_{}'.format(str(cf.name))
         initial = cf.default if not bulk_edit else None
         initial = cf.default if not bulk_edit else None
 
 
         # Integer
         # Integer
-        if cf.type == CF_TYPE_INTEGER:
+        if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
             field = forms.IntegerField(required=cf.required, initial=initial)
             field = forms.IntegerField(required=cf.required, initial=initial)
 
 
         # Boolean
         # Boolean
-        elif cf.type == CF_TYPE_BOOLEAN:
+        elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
             choices = (
             choices = (
                 (None, '---------'),
                 (None, '---------'),
                 (1, 'True'),
                 (1, 'True'),
@@ -56,11 +57,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
             )
             )
 
 
         # Date
         # Date
-        elif cf.type == CF_TYPE_DATE:
+        elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
             field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
             field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
 
 
         # Select
         # Select
-        elif cf.type == CF_TYPE_SELECT:
+        elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
             choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
             choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
             if not cf.required or bulk_edit or filterable_only:
             if not cf.required or bulk_edit or filterable_only:
                 choices = [(None, '---------')] + choices
                 choices = [(None, '---------')] + choices
@@ -74,7 +75,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
             field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
             field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
 
 
         # URL
         # URL
-        elif cf.type == CF_TYPE_URL:
+        elif cf.type == CustomFieldTypeChoices.TYPE_URL:
             field = LaxURLField(required=cf.required, initial=initial)
             field = LaxURLField(required=cf.required, initial=initial)
 
 
         # Text
         # Text
@@ -400,7 +401,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
         )
         )
     )
     )
     action = forms.ChoiceField(
     action = forms.ChoiceField(
-        choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
+        choices=add_blank_choice(ObjectChangeActionChoices),
         required=False
         required=False
     )
     )
     user = forms.ModelChoiceField(
     user = forms.ModelChoiceField(

+ 7 - 7
netbox/extras/middleware.py

@@ -10,7 +10,7 @@ from django.utils import timezone
 from django_prometheus.models import model_deletes, model_inserts, model_updates
 from django_prometheus.models import model_deletes, model_inserts, model_updates
 
 
 from utilities.querysets import DummyQuerySet
 from utilities.querysets import DummyQuerySet
-from .constants import *
+from .choices import ObjectChangeActionChoices
 from .models import ObjectChange
 from .models import ObjectChange
 from .signals import purge_changelog
 from .signals import purge_changelog
 from .webhooks import enqueue_webhooks
 from .webhooks import enqueue_webhooks
@@ -23,7 +23,7 @@ def handle_changed_object(sender, instance, **kwargs):
     Fires when an object is created or updated.
     Fires when an object is created or updated.
     """
     """
     # Queue the object for processing once the request completes
     # Queue the object for processing once the request completes
-    action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
+    action = ObjectChangeActionChoices.ACTION_CREATE if kwargs['created'] else ObjectChangeActionChoices.ACTION_UPDATE
     _thread_locals.changed_objects.append(
     _thread_locals.changed_objects.append(
         (instance, action)
         (instance, action)
     )
     )
@@ -46,7 +46,7 @@ def handle_deleted_object(sender, instance, **kwargs):
 
 
     # Queue the copy of the object for processing once the request completes
     # Queue the copy of the object for processing once the request completes
     _thread_locals.changed_objects.append(
     _thread_locals.changed_objects.append(
-        (copy, OBJECTCHANGE_ACTION_DELETE)
+        (copy, ObjectChangeActionChoices.ACTION_DELETE)
     )
     )
 
 
 
 
@@ -101,7 +101,7 @@ class ObjectChangeMiddleware(object):
         for instance, action in _thread_locals.changed_objects:
         for instance, action in _thread_locals.changed_objects:
 
 
             # Refresh cached custom field values
             # Refresh cached custom field values
-            if action in [OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_UPDATE]:
+            if action in [ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]:
                 if hasattr(instance, 'cache_custom_fields'):
                 if hasattr(instance, 'cache_custom_fields'):
                     instance.cache_custom_fields()
                     instance.cache_custom_fields()
 
 
@@ -116,11 +116,11 @@ class ObjectChangeMiddleware(object):
             enqueue_webhooks(instance, request.user, request.id, action)
             enqueue_webhooks(instance, request.user, request.id, action)
 
 
             # Increment metric counters
             # Increment metric counters
-            if action == OBJECTCHANGE_ACTION_CREATE:
+            if action == ObjectChangeActionChoices.ACTION_CREATE:
                 model_inserts.labels(instance._meta.model_name).inc()
                 model_inserts.labels(instance._meta.model_name).inc()
-            elif action == OBJECTCHANGE_ACTION_UPDATE:
+            elif action == ObjectChangeActionChoices.ACTION_UPDATE:
                 model_updates.labels(instance._meta.model_name).inc()
                 model_updates.labels(instance._meta.model_name).inc()
-            elif action == OBJECTCHANGE_ACTION_DELETE:
+            elif action == ObjectChangeActionChoices.ACTION_DELETE:
                 model_deletes.labels(instance._meta.model_name).inc()
                 model_deletes.labels(instance._meta.model_name).inc()
 
 
         # Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in
         # Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in

+ 0 - 2
netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py

@@ -8,8 +8,6 @@ import django.db.models.deletion
 import extras.models
 import extras.models
 from django.db.utils import OperationalError
 from django.db.utils import OperationalError
 
 
-from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
-
 
 
 def verify_postgresql_version(apps, schema_editor):
 def verify_postgresql_version(apps, schema_editor):
     """
     """

+ 5 - 7
netbox/extras/migrations/0010_customfield_filter_logic.py

@@ -2,21 +2,19 @@
 # Generated by Django 1.11.9 on 2018-02-21 19:48
 # Generated by Django 1.11.9 on 2018-02-21 19:48
 from django.db import migrations, models
 from django.db import migrations, models
 
 
-from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
-
 
 
 def is_filterable_to_filter_logic(apps, schema_editor):
 def is_filterable_to_filter_logic(apps, schema_editor):
     CustomField = apps.get_model('extras', 'CustomField')
     CustomField = apps.get_model('extras', 'CustomField')
-    CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
-    CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
+    CustomField.objects.filter(is_filterable=False).update(filter_logic=0)
+    CustomField.objects.filter(is_filterable=True).update(filter_logic=1)
     # Select fields match on primary key only
     # Select fields match on primary key only
-    CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
+    CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2)
 
 
 
 
 def filter_logic_to_is_filterable(apps, schema_editor):
 def filter_logic_to_is_filterable(apps, schema_editor):
     CustomField = apps.get_model('extras', 'CustomField')
     CustomField = apps.get_model('extras', 'CustomField')
-    CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
-    CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
+    CustomField.objects.filter(filter_logic=0).update(is_filterable=False)
+    CustomField.objects.exclude(filter_logic=0).update(is_filterable=True)
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 69 - 0
netbox/extras/migrations/0029_3569_customfield_fields.py

@@ -0,0 +1,69 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+CUSTOMFIELD_TYPE_CHOICES = (
+    (100, 'text'),
+    (200, 'integer'),
+    (300, 'boolean'),
+    (400, 'date'),
+    (500, 'url'),
+    (600, 'select')
+)
+
+CUSTOMFIELD_FILTER_LOGIC_CHOICES = (
+    (0, 'disabled'),
+    (1, 'integer'),
+    (2, 'exact'),
+)
+
+
+def customfield_type_to_slug(apps, schema_editor):
+    CustomField = apps.get_model('extras', 'CustomField')
+    for id, slug in CUSTOMFIELD_TYPE_CHOICES:
+        CustomField.objects.filter(type=str(id)).update(type=slug)
+
+
+def customfield_filter_logic_to_slug(apps, schema_editor):
+    CustomField = apps.get_model('extras', 'CustomField')
+    for id, slug in CUSTOMFIELD_FILTER_LOGIC_CHOICES:
+        CustomField.objects.filter(filter_logic=str(id)).update(filter_logic=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('extras', '0028_remove_topology_maps'),
+    ]
+
+    operations = [
+
+        # CustomField.type
+        migrations.AlterField(
+            model_name='customfield',
+            name='type',
+            field=models.CharField(default='text', max_length=50),
+        ),
+        migrations.RunPython(
+            code=customfield_type_to_slug
+        ),
+
+        # Update CustomFieldChoice.field.limit_choices_to
+        migrations.AlterField(
+            model_name='customfieldchoice',
+            name='field',
+            field=models.ForeignKey(limit_choices_to={'type': 'select'}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
+        ),
+
+        # CustomField.filter_logic
+        migrations.AlterField(
+            model_name='customfield',
+            name='filter_logic',
+            field=models.CharField(default='loose', max_length=50),
+        ),
+        migrations.RunPython(
+            code=customfield_filter_logic_to_slug
+        ),
+
+    ]

+ 36 - 0
netbox/extras/migrations/0030_3569_objectchange_fields.py

@@ -0,0 +1,36 @@
+from django.db import migrations, models
+
+
+OBJECTCHANGE_ACTION_CHOICES = (
+    (1, 'create'),
+    (2, 'update'),
+    (3, 'delete'),
+)
+
+
+def objectchange_action_to_slug(apps, schema_editor):
+    ObjectChange = apps.get_model('extras', 'ObjectChange')
+    for id, slug in OBJECTCHANGE_ACTION_CHOICES:
+        ObjectChange.objects.filter(action=str(id)).update(action=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('extras', '0029_3569_customfield_fields'),
+    ]
+
+    operations = [
+
+        # ObjectChange.action
+        migrations.AlterField(
+            model_name='objectchange',
+            name='action',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=objectchange_action_to_slug
+        ),
+
+    ]

+ 35 - 0
netbox/extras/migrations/0031_3569_exporttemplate_fields.py

@@ -0,0 +1,35 @@
+from django.db import migrations, models
+
+
+EXPORTTEMPLATE_LANGUAGE_CHOICES = (
+    (10, 'django'),
+    (20, 'jinja2'),
+)
+
+
+def exporttemplate_language_to_slug(apps, schema_editor):
+    ExportTemplate = apps.get_model('extras', 'ExportTemplate')
+    for id, slug in EXPORTTEMPLATE_LANGUAGE_CHOICES:
+        ExportTemplate.objects.filter(template_language=str(id)).update(template_language=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('extras', '0030_3569_objectchange_fields'),
+    ]
+
+    operations = [
+
+        # ExportTemplate.template_language
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='template_language',
+            field=models.CharField(default='jinja2', max_length=50),
+        ),
+        migrations.RunPython(
+            code=exporttemplate_language_to_slug
+        ),
+
+    ]

+ 35 - 0
netbox/extras/migrations/0032_3569_webhook_fields.py

@@ -0,0 +1,35 @@
+from django.db import migrations, models
+
+
+WEBHOOK_CONTENTTYPE_CHOICES = (
+    (1, 'application/json'),
+    (2, 'application/x-www-form-urlencoded'),
+)
+
+
+def webhook_contenttype_to_slug(apps, schema_editor):
+    Webhook = apps.get_model('extras', 'Webhook')
+    for id, slug in WEBHOOK_CONTENTTYPE_CHOICES:
+        Webhook.objects.filter(http_content_type=str(id)).update(http_content_type=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('extras', '0031_3569_exporttemplate_fields'),
+    ]
+
+    operations = [
+
+        # Webhook.http_content_type
+        migrations.AlterField(
+            model_name='webhook',
+            name='http_content_type',
+            field=models.CharField(default='application/json', max_length=50),
+        ),
+        migrations.RunPython(
+            code=webhook_contenttype_to_slug
+        ),
+
+    ]

+ 35 - 26
netbox/extras/models.py

@@ -15,6 +15,7 @@ from taggit.models import TagBase, GenericTaggedItemBase
 
 
 from utilities.fields import ColorField
 from utilities.fields import ColorField
 from utilities.utils import deepmerge, model_names_to_filter_dict
 from utilities.utils import deepmerge, model_names_to_filter_dict
+from .choices import *
 from .constants import *
 from .constants import *
 from .querysets import ConfigContextQuerySet
 from .querysets import ConfigContextQuerySet
 
 
@@ -62,9 +63,10 @@ class Webhook(models.Model):
         verbose_name='URL',
         verbose_name='URL',
         help_text="A POST will be sent to this URL when the webhook is called."
         help_text="A POST will be sent to this URL when the webhook is called."
     )
     )
-    http_content_type = models.PositiveSmallIntegerField(
-        choices=WEBHOOK_CT_CHOICES,
-        default=WEBHOOK_CT_JSON,
+    http_content_type = models.CharField(
+        max_length=50,
+        choices=WebhookContentTypeChoices,
+        default=WebhookContentTypeChoices.CONTENTTYPE_JSON,
         verbose_name='HTTP content type'
         verbose_name='HTTP content type'
     )
     )
     additional_headers = JSONField(
     additional_headers = JSONField(
@@ -182,9 +184,10 @@ class CustomField(models.Model):
         limit_choices_to=get_custom_field_models,
         limit_choices_to=get_custom_field_models,
         help_text='The object(s) to which this field applies.'
         help_text='The object(s) to which this field applies.'
     )
     )
-    type = models.PositiveSmallIntegerField(
-        choices=CUSTOMFIELD_TYPE_CHOICES,
-        default=CF_TYPE_TEXT
+    type = models.CharField(
+        max_length=50,
+        choices=CustomFieldTypeChoices,
+        default=CustomFieldTypeChoices.TYPE_TEXT
     )
     )
     name = models.CharField(
     name = models.CharField(
         max_length=50,
         max_length=50,
@@ -205,9 +208,10 @@ class CustomField(models.Model):
         help_text='If true, this field is required when creating new objects '
         help_text='If true, this field is required when creating new objects '
                   'or editing an existing object.'
                   'or editing an existing object.'
     )
     )
-    filter_logic = models.PositiveSmallIntegerField(
-        choices=CF_FILTER_CHOICES,
-        default=CF_FILTER_LOOSE,
+    filter_logic = models.CharField(
+        max_length=50,
+        choices=CustomFieldFilterLogicChoices,
+        default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
         help_text='Loose matches any instance of a given string; exact '
         help_text='Loose matches any instance of a given string; exact '
                   'matches the entire field.'
                   'matches the entire field.'
     )
     )
@@ -233,15 +237,15 @@ class CustomField(models.Model):
         """
         """
         if value is None:
         if value is None:
             return ''
             return ''
-        if self.type == CF_TYPE_BOOLEAN:
+        if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
             return str(int(bool(value)))
             return str(int(bool(value)))
-        if self.type == CF_TYPE_DATE:
+        if self.type == CustomFieldTypeChoices.TYPE_DATE:
             # Could be date/datetime object or string
             # Could be date/datetime object or string
             try:
             try:
                 return value.strftime('%Y-%m-%d')
                 return value.strftime('%Y-%m-%d')
             except AttributeError:
             except AttributeError:
                 return value
                 return value
-        if self.type == CF_TYPE_SELECT:
+        if self.type == CustomFieldTypeChoices.TYPE_SELECT:
             # Could be ModelChoiceField or TypedChoiceField
             # Could be ModelChoiceField or TypedChoiceField
             return str(value.id) if hasattr(value, 'id') else str(value)
             return str(value.id) if hasattr(value, 'id') else str(value)
         return value
         return value
@@ -252,14 +256,14 @@ class CustomField(models.Model):
         """
         """
         if serialized_value == '':
         if serialized_value == '':
             return None
             return None
-        if self.type == CF_TYPE_INTEGER:
+        if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
             return int(serialized_value)
             return int(serialized_value)
-        if self.type == CF_TYPE_BOOLEAN:
+        if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
             return bool(int(serialized_value))
             return bool(int(serialized_value))
-        if self.type == CF_TYPE_DATE:
+        if self.type == CustomFieldTypeChoices.TYPE_DATE:
             # Read date as YYYY-MM-DD
             # Read date as YYYY-MM-DD
             return date(*[int(n) for n in serialized_value.split('-')])
             return date(*[int(n) for n in serialized_value.split('-')])
-        if self.type == CF_TYPE_SELECT:
+        if self.type == CustomFieldTypeChoices.TYPE_SELECT:
             return self.choices.get(pk=int(serialized_value))
             return self.choices.get(pk=int(serialized_value))
         return serialized_value
         return serialized_value
 
 
@@ -312,7 +316,7 @@ class CustomFieldChoice(models.Model):
         to='extras.CustomField',
         to='extras.CustomField',
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
         related_name='choices',
         related_name='choices',
-        limit_choices_to={'type': CF_TYPE_SELECT}
+        limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
     )
     )
     value = models.CharField(
     value = models.CharField(
         max_length=100
         max_length=100
@@ -330,14 +334,17 @@ class CustomFieldChoice(models.Model):
         return self.value
         return self.value
 
 
     def clean(self):
     def clean(self):
-        if self.field.type != CF_TYPE_SELECT:
+        if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
             raise ValidationError("Custom field choices can only be assigned to selection fields.")
             raise ValidationError("Custom field choices can only be assigned to selection fields.")
 
 
     def delete(self, using=None, keep_parents=False):
     def delete(self, using=None, keep_parents=False):
         # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
         # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
         pk = self.pk
         pk = self.pk
         super().delete(using, keep_parents)
         super().delete(using, keep_parents)
-        CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
+        CustomFieldValue.objects.filter(
+            field__type=CustomFieldTypeChoices.TYPE_SELECT,
+            serialized_value=str(pk)
+        ).delete()
 
 
 
 
 #
 #
@@ -381,8 +388,8 @@ class CustomLink(models.Model):
     )
     )
     button_class = models.CharField(
     button_class = models.CharField(
         max_length=30,
         max_length=30,
-        choices=BUTTON_CLASS_CHOICES,
-        default=BUTTON_CLASS_DEFAULT,
+        choices=CustomLinkButtonClassChoices,
+        default=CustomLinkButtonClassChoices.CLASS_DEFAULT,
         help_text="The class of the first link in a group will be used for the dropdown button"
         help_text="The class of the first link in a group will be used for the dropdown button"
     )
     )
     new_window = models.BooleanField(
     new_window = models.BooleanField(
@@ -458,9 +465,10 @@ class ExportTemplate(models.Model):
         max_length=200,
         max_length=200,
         blank=True
         blank=True
     )
     )
-    template_language = models.PositiveSmallIntegerField(
-        choices=TEMPLATE_LANGUAGE_CHOICES,
-        default=TEMPLATE_LANGUAGE_JINJA2
+    template_language = models.CharField(
+        max_length=50,
+        choices=ExportTemplateLanguageChoices,
+        default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
     )
     )
     template_code = models.TextField(
     template_code = models.TextField(
         help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.'
         help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.'
@@ -796,8 +804,9 @@ class ObjectChange(models.Model):
     request_id = models.UUIDField(
     request_id = models.UUIDField(
         editable=False
         editable=False
     )
     )
-    action = models.PositiveSmallIntegerField(
-        choices=OBJECTCHANGE_ACTION_CHOICES
+    action = models.CharField(
+        max_length=50,
+        choices=ObjectChangeActionChoices
     )
     )
     changed_object_type = models.ForeignKey(
     changed_object_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,

+ 3 - 3
netbox/extras/tables.py

@@ -38,11 +38,11 @@ OBJECTCHANGE_TIME = """
 """
 """
 
 
 OBJECTCHANGE_ACTION = """
 OBJECTCHANGE_ACTION = """
-{% if record.action == 1 %}
+{% if record.action == 'create' %}
     <span class="label label-success">Created</span>
     <span class="label label-success">Created</span>
-{% elif record.action == 2 %}
+{% elif record.action == 'update' %}
     <span class="label label-primary">Updated</span>
     <span class="label label-primary">Updated</span>
-{% elif record.action == 3 %}
+{% elif record.action == 'delete' %}
     <span class="label label-danger">Deleted</span>
     <span class="label label-danger">Deleted</span>
 {% endif %}
 {% endif %}
 """
 """

+ 5 - 4
netbox/extras/tests/test_changelog.py

@@ -3,6 +3,7 @@ from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
 from dcim.models import Site
 from dcim.models import Site
+from extras.choices import *
 from extras.constants import *
 from extras.constants import *
 from extras.models import CustomField, CustomFieldValue, ObjectChange
 from extras.models import CustomField, CustomFieldValue, ObjectChange
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
@@ -17,7 +18,7 @@ class ChangeLogTest(APITestCase):
         # Create a custom field on the Site model
         # Create a custom field on the Site model
         ct = ContentType.objects.get_for_model(Site)
         ct = ContentType.objects.get_for_model(Site)
         cf = CustomField(
         cf = CustomField(
-            type=CF_TYPE_TEXT,
+            type=CustomFieldTypeChoices.TYPE_TEXT,
             name='my_field',
             name='my_field',
             required=False
             required=False
         )
         )
@@ -49,7 +50,7 @@ class ChangeLogTest(APITestCase):
             changed_object_id=site.pk
             changed_object_id=site.pk
         )
         )
         self.assertEqual(oc.changed_object, site)
         self.assertEqual(oc.changed_object, site)
-        self.assertEqual(oc.action, OBJECTCHANGE_ACTION_CREATE)
+        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE)
         self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
         self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
         self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
         self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
 
 
@@ -81,7 +82,7 @@ class ChangeLogTest(APITestCase):
             changed_object_id=site.pk
             changed_object_id=site.pk
         )
         )
         self.assertEqual(oc.changed_object, site)
         self.assertEqual(oc.changed_object, site)
-        self.assertEqual(oc.action, OBJECTCHANGE_ACTION_UPDATE)
+        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
         self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
         self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
         self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
         self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
 
 
@@ -110,6 +111,6 @@ class ChangeLogTest(APITestCase):
         oc = ObjectChange.objects.first()
         oc = ObjectChange.objects.first()
         self.assertEqual(oc.changed_object, None)
         self.assertEqual(oc.changed_object, None)
         self.assertEqual(oc.object_repr, site.name)
         self.assertEqual(oc.object_repr, site.name)
-        self.assertEqual(oc.action, OBJECTCHANGE_ACTION_DELETE)
+        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE)
         self.assertEqual(oc.object_data['custom_fields'], {'my_field': 'ABC'})
         self.assertEqual(oc.object_data['custom_fields'], {'my_field': 'ABC'})
         self.assertListEqual(sorted(oc.object_data['tags']), ['bar', 'foo'])
         self.assertListEqual(sorted(oc.object_data['tags']), ['bar', 'foo'])

+ 17 - 17
netbox/extras/tests/test_customfields.py

@@ -6,7 +6,7 @@ from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
 from dcim.models import Site
 from dcim.models import Site
-from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, CF_TYPE_SELECT
+from extras.choices import *
 from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
 from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
@@ -25,13 +25,13 @@ class CustomFieldTest(TestCase):
     def test_simple_fields(self):
     def test_simple_fields(self):
 
 
         DATA = (
         DATA = (
-            {'field_type': CF_TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
-            {'field_type': CF_TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
-            {'field_type': CF_TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
-            {'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
-            {'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
-            {'field_type': CF_TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None},
-            {'field_type': CF_TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
+            {'field_type': CustomFieldTypeChoices.TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
+            {'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
+            {'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
+            {'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
+            {'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
+            {'field_type': CustomFieldTypeChoices.TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None},
+            {'field_type': CustomFieldTypeChoices.TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
         )
         )
 
 
         obj_type = ContentType.objects.get_for_model(Site)
         obj_type = ContentType.objects.get_for_model(Site)
@@ -67,7 +67,7 @@ class CustomFieldTest(TestCase):
         obj_type = ContentType.objects.get_for_model(Site)
         obj_type = ContentType.objects.get_for_model(Site)
 
 
         # Create a custom field
         # Create a custom field
-        cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False)
+        cf = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='my_field', required=False)
         cf.save()
         cf.save()
         cf.obj_type.set([obj_type])
         cf.obj_type.set([obj_type])
         cf.save()
         cf.save()
@@ -107,37 +107,37 @@ class CustomFieldAPITest(APITestCase):
         content_type = ContentType.objects.get_for_model(Site)
         content_type = ContentType.objects.get_for_model(Site)
 
 
         # Text custom field
         # Text custom field
-        self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word')
+        self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='magic_word')
         self.cf_text.save()
         self.cf_text.save()
         self.cf_text.obj_type.set([content_type])
         self.cf_text.obj_type.set([content_type])
         self.cf_text.save()
         self.cf_text.save()
 
 
         # Integer custom field
         # Integer custom field
-        self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
+        self.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='magic_number')
         self.cf_integer.save()
         self.cf_integer.save()
         self.cf_integer.obj_type.set([content_type])
         self.cf_integer.obj_type.set([content_type])
         self.cf_integer.save()
         self.cf_integer.save()
 
 
         # Boolean custom field
         # Boolean custom field
-        self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
+        self.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='is_magic')
         self.cf_boolean.save()
         self.cf_boolean.save()
         self.cf_boolean.obj_type.set([content_type])
         self.cf_boolean.obj_type.set([content_type])
         self.cf_boolean.save()
         self.cf_boolean.save()
 
 
         # Date custom field
         # Date custom field
-        self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
+        self.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='magic_date')
         self.cf_date.save()
         self.cf_date.save()
         self.cf_date.obj_type.set([content_type])
         self.cf_date.obj_type.set([content_type])
         self.cf_date.save()
         self.cf_date.save()
 
 
         # URL custom field
         # URL custom field
-        self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
+        self.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='magic_url')
         self.cf_url.save()
         self.cf_url.save()
         self.cf_url.obj_type.set([content_type])
         self.cf_url.obj_type.set([content_type])
         self.cf_url.save()
         self.cf_url.save()
 
 
         # Select custom field
         # Select custom field
-        self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
+        self.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='magic_choice')
         self.cf_select.save()
         self.cf_select.save()
         self.cf_select.obj_type.set([content_type])
         self.cf_select.obj_type.set([content_type])
         self.cf_select.save()
         self.cf_select.save()
@@ -308,8 +308,8 @@ class CustomFieldChoiceAPITest(APITestCase):
 
 
         vm_content_type = ContentType.objects.get_for_model(VirtualMachine)
         vm_content_type = ContentType.objects.get_for_model(VirtualMachine)
 
 
-        self.cf_1 = CustomField.objects.create(name="cf_1", type=CF_TYPE_SELECT)
-        self.cf_2 = CustomField.objects.create(name="cf_2", type=CF_TYPE_SELECT)
+        self.cf_1 = CustomField.objects.create(name="cf_1", type=CustomFieldTypeChoices.TYPE_SELECT)
+        self.cf_2 = CustomField.objects.create(name="cf_2", type=CustomFieldTypeChoices.TYPE_SELECT)
 
 
         self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100)
         self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100)
         self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50)
         self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50)

+ 2 - 2
netbox/extras/tests/test_views.py

@@ -6,7 +6,7 @@ from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Site
 from dcim.models import Site
-from extras.constants import OBJECTCHANGE_ACTION_UPDATE
+from extras.choices import ObjectChangeActionChoices
 from extras.models import ConfigContext, ObjectChange, Tag
 from extras.models import ConfigContext, ObjectChange, Tag
 from utilities.testing import create_test_user
 from utilities.testing import create_test_user
 
 
@@ -83,7 +83,7 @@ class ObjectChangeTestCase(TestCase):
 
 
         # Create three ObjectChanges
         # Create three ObjectChanges
         for i in range(1, 4):
         for i in range(1, 4):
-            oc = site.to_objectchange(action=OBJECTCHANGE_ACTION_UPDATE)
+            oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
             oc.user = user
             oc.user = user
             oc.request_id = uuid.uuid4()
             oc.request_id = uuid.uuid4()
             oc.save()
             oc.save()

+ 4 - 3
netbox/extras/webhooks.py

@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
 
 
 from extras.models import Webhook
 from extras.models import Webhook
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
+from .choices import *
 from .constants import *
 from .constants import *
 
 
 
 
@@ -18,9 +19,9 @@ def enqueue_webhooks(instance, user, request_id, action):
 
 
     # Retrieve any applicable Webhooks
     # Retrieve any applicable Webhooks
     action_flag = {
     action_flag = {
-        OBJECTCHANGE_ACTION_CREATE: 'type_create',
-        OBJECTCHANGE_ACTION_UPDATE: 'type_update',
-        OBJECTCHANGE_ACTION_DELETE: 'type_delete',
+        ObjectChangeActionChoices.ACTION_CREATE: 'type_create',
+        ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',
+        ObjectChangeActionChoices.ACTION_DELETE: 'type_delete',
     }[action]
     }[action]
     obj_type = ContentType.objects.get_for_model(instance.__class__)
     obj_type = ContentType.objects.get_for_model(instance.__class__)
     webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
     webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})

+ 2 - 1
netbox/extras/webhooks_worker.py

@@ -6,6 +6,7 @@ import requests
 from django_rq import job
 from django_rq import job
 from rest_framework.utils.encoders import JSONEncoder
 from rest_framework.utils.encoders import JSONEncoder
 
 
+from .choices import ObjectChangeActionChoices
 from .constants import *
 from .constants import *
 
 
 
 
@@ -15,7 +16,7 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
     Make a POST request to the defined Webhook
     Make a POST request to the defined Webhook
     """
     """
     payload = {
     payload = {
-        'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
+        'event': dict(ObjectChangeActionChoices)[event].lower(),
         'timestamp': timestamp,
         'timestamp': timestamp,
         'model': model_name,
         'model': model_name,
         'username': username,
         'username': username,

+ 7 - 7
netbox/ipam/api/serializers.py

@@ -8,8 +8,8 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
 from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
 from dcim.models import Interface
 from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
-from ipam.constants import *
-from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
+from ipam.choices import *
+from ipam.models import AF_CHOICES, Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from utilities.api import (
 from utilities.api import (
     ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
     ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
@@ -102,7 +102,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     group = NestedVLANGroupSerializer(required=False, allow_null=True)
     group = NestedVLANGroupSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=VLAN_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=VLANStatusChoices, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
     role = NestedRoleSerializer(required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
     prefix_count = serializers.IntegerField(read_only=True)
     prefix_count = serializers.IntegerField(read_only=True)
@@ -140,7 +140,7 @@ class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=PREFIX_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=PrefixStatusChoices, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
     role = NestedRoleSerializer(required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
@@ -200,8 +200,8 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
     family = ChoiceField(choices=AF_CHOICES, read_only=True)
     family = ChoiceField(choices=AF_CHOICES, read_only=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
-    role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
+    status = ChoiceField(choices=IPAddressStatusChoices, required=False)
+    role = ChoiceField(choices=IPAddressRoleChoices, required=False, allow_null=True)
     interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
     interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
     nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
     nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
     nat_outside = NestedIPAddressSerializer(read_only=True)
     nat_outside = NestedIPAddressSerializer(read_only=True)
@@ -239,7 +239,7 @@ class AvailableIPSerializer(serializers.Serializer):
 class ServiceSerializer(CustomFieldModelSerializer):
 class ServiceSerializer(CustomFieldModelSerializer):
     device = NestedDeviceSerializer(required=False, allow_null=True)
     device = NestedDeviceSerializer(required=False, allow_null=True)
     virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
     virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
-    protocol = ChoiceField(choices=IP_PROTOCOL_CHOICES)
+    protocol = ChoiceField(choices=ServiceProtocolChoices)
     ipaddresses = SerializedPKRelatedField(
     ipaddresses = SerializedPKRelatedField(
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
         serializer=NestedIPAddressSerializer,
         serializer=NestedIPAddressSerializer,

+ 130 - 0
netbox/ipam/choices.py

@@ -0,0 +1,130 @@
+from utilities.choices import ChoiceSet
+
+
+#
+# Prefixes
+#
+
+class PrefixStatusChoices(ChoiceSet):
+
+    STATUS_CONTAINER = 'container'
+    STATUS_ACTIVE = 'active'
+    STATUS_RESERVED = 'reserved'
+    STATUS_DEPRECATED = 'deprecated'
+
+    CHOICES = (
+        (STATUS_CONTAINER, 'Container'),
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_RESERVED, 'Reserved'),
+        (STATUS_DEPRECATED, 'Deprecated'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_CONTAINER: 0,
+        STATUS_ACTIVE: 1,
+        STATUS_RESERVED: 2,
+        STATUS_DEPRECATED: 3,
+    }
+
+
+#
+# IPAddresses
+#
+
+class IPAddressStatusChoices(ChoiceSet):
+
+    STATUS_ACTIVE = 'active'
+    STATUS_RESERVED = 'reserved'
+    STATUS_DEPRECATED = 'deprecated'
+    STATUS_DHCP = 'dhcp'
+
+    CHOICES = (
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_RESERVED, 'Reserved'),
+        (STATUS_DEPRECATED, 'Deprecated'),
+        (STATUS_DHCP, 'DHCP'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_ACTIVE: 1,
+        STATUS_RESERVED: 2,
+        STATUS_DEPRECATED: 3,
+        STATUS_DHCP: 5,
+    }
+
+
+class IPAddressRoleChoices(ChoiceSet):
+
+    ROLE_LOOPBACK = 'loopback'
+    ROLE_SECONDARY = 'secondary'
+    ROLE_ANYCAST = 'anycast'
+    ROLE_VIP = 'vip'
+    ROLE_VRRP = 'vrrp'
+    ROLE_HSRP = 'hsrp'
+    ROLE_GLBP = 'glbp'
+    ROLE_CARP = 'carp'
+
+    CHOICES = (
+        (ROLE_LOOPBACK, 'Loopback'),
+        (ROLE_SECONDARY, 'Secondary'),
+        (ROLE_ANYCAST, 'Anycast'),
+        (ROLE_VIP, 'VIP'),
+        (ROLE_VRRP, 'VRRP'),
+        (ROLE_HSRP, 'HSRP'),
+        (ROLE_GLBP, 'GLBP'),
+        (ROLE_CARP, 'CARP'),
+    )
+
+    LEGACY_MAP = {
+        ROLE_LOOPBACK: 10,
+        ROLE_SECONDARY: 20,
+        ROLE_ANYCAST: 30,
+        ROLE_VIP: 40,
+        ROLE_VRRP: 41,
+        ROLE_HSRP: 42,
+        ROLE_GLBP: 43,
+        ROLE_CARP: 44,
+    }
+
+
+#
+# VLANs
+#
+
+class VLANStatusChoices(ChoiceSet):
+
+    STATUS_ACTIVE = 'active'
+    STATUS_RESERVED = 'reserved'
+    STATUS_DEPRECATED = 'deprecated'
+
+    CHOICES = (
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_RESERVED, 'Reserved'),
+        (STATUS_DEPRECATED, 'Deprecated'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_ACTIVE: 1,
+        STATUS_RESERVED: 2,
+        STATUS_DEPRECATED: 3,
+    }
+
+
+#
+# VLANs
+#
+
+class ServiceProtocolChoices(ChoiceSet):
+
+    PROTOCOL_TCP = 'tcp'
+    PROTOCOL_UDP = 'udp'
+
+    CHOICES = (
+        (PROTOCOL_TCP, 'TCP'),
+        (PROTOCOL_UDP, 'UDP'),
+    )
+
+    LEGACY_MAP = {
+        PROTOCOL_TCP: 6,
+        PROTOCOL_UDP: 17,
+    }

+ 0 - 97
netbox/ipam/constants.py

@@ -1,97 +0,0 @@
-# IP address families
-AF_CHOICES = (
-    (4, 'IPv4'),
-    (6, 'IPv6'),
-)
-
-# Prefix statuses
-PREFIX_STATUS_CONTAINER = 0
-PREFIX_STATUS_ACTIVE = 1
-PREFIX_STATUS_RESERVED = 2
-PREFIX_STATUS_DEPRECATED = 3
-PREFIX_STATUS_CHOICES = (
-    (PREFIX_STATUS_CONTAINER, 'Container'),
-    (PREFIX_STATUS_ACTIVE, 'Active'),
-    (PREFIX_STATUS_RESERVED, 'Reserved'),
-    (PREFIX_STATUS_DEPRECATED, 'Deprecated')
-)
-
-# IP address statuses
-IPADDRESS_STATUS_ACTIVE = 1
-IPADDRESS_STATUS_RESERVED = 2
-IPADDRESS_STATUS_DEPRECATED = 3
-IPADDRESS_STATUS_DHCP = 5
-IPADDRESS_STATUS_CHOICES = (
-    (IPADDRESS_STATUS_ACTIVE, 'Active'),
-    (IPADDRESS_STATUS_RESERVED, 'Reserved'),
-    (IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
-    (IPADDRESS_STATUS_DHCP, 'DHCP')
-)
-
-# IP address roles
-IPADDRESS_ROLE_LOOPBACK = 10
-IPADDRESS_ROLE_SECONDARY = 20
-IPADDRESS_ROLE_ANYCAST = 30
-IPADDRESS_ROLE_VIP = 40
-IPADDRESS_ROLE_VRRP = 41
-IPADDRESS_ROLE_HSRP = 42
-IPADDRESS_ROLE_GLBP = 43
-IPADDRESS_ROLE_CARP = 44
-IPADDRESS_ROLE_CHOICES = (
-    (IPADDRESS_ROLE_LOOPBACK, 'Loopback'),
-    (IPADDRESS_ROLE_SECONDARY, 'Secondary'),
-    (IPADDRESS_ROLE_ANYCAST, 'Anycast'),
-    (IPADDRESS_ROLE_VIP, 'VIP'),
-    (IPADDRESS_ROLE_VRRP, 'VRRP'),
-    (IPADDRESS_ROLE_HSRP, 'HSRP'),
-    (IPADDRESS_ROLE_GLBP, 'GLBP'),
-    (IPADDRESS_ROLE_CARP, 'CARP'),
-)
-
-IPADDRESS_ROLES_NONUNIQUE = (
-    # IPAddress roles which are exempt from unique address enforcement
-    IPADDRESS_ROLE_ANYCAST,
-    IPADDRESS_ROLE_VIP,
-    IPADDRESS_ROLE_VRRP,
-    IPADDRESS_ROLE_HSRP,
-    IPADDRESS_ROLE_GLBP,
-    IPADDRESS_ROLE_CARP,
-)
-
-# VLAN statuses
-VLAN_STATUS_ACTIVE = 1
-VLAN_STATUS_RESERVED = 2
-VLAN_STATUS_DEPRECATED = 3
-VLAN_STATUS_CHOICES = (
-    (VLAN_STATUS_ACTIVE, 'Active'),
-    (VLAN_STATUS_RESERVED, 'Reserved'),
-    (VLAN_STATUS_DEPRECATED, 'Deprecated')
-)
-
-# Bootstrap CSS classes
-STATUS_CHOICE_CLASSES = {
-    0: 'default',
-    1: 'primary',
-    2: 'info',
-    3: 'danger',
-    4: 'warning',
-    5: 'success',
-}
-ROLE_CHOICE_CLASSES = {
-    10: 'default',
-    20: 'primary',
-    30: 'warning',
-    40: 'success',
-    41: 'success',
-    42: 'success',
-    43: 'success',
-    44: 'success',
-}
-
-# IP protocols (for services)
-IP_PROTOCOL_TCP = 6
-IP_PROTOCOL_UDP = 17
-IP_PROTOCOL_CHOICES = (
-    (IP_PROTOCOL_TCP, 'TCP'),
-    (IP_PROTOCOL_UDP, 'UDP'),
-)

+ 5 - 5
netbox/ipam/filters.py

@@ -9,7 +9,7 @@ from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
-from .constants import *
+from .choices import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
@@ -178,7 +178,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
         label='Role (slug)',
         label='Role (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=PREFIX_STATUS_CHOICES,
+        choices=PrefixStatusChoices,
         null_value=None
         null_value=None
     )
     )
     tag = TagFilter()
     tag = TagFilter()
@@ -310,11 +310,11 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
         label='Interface (ID)',
         label='Interface (ID)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=IPADDRESS_STATUS_CHOICES,
+        choices=IPAddressStatusChoices,
         null_value=None
         null_value=None
     )
     )
     role = django_filters.MultipleChoiceFilter(
     role = django_filters.MultipleChoiceFilter(
-        choices=IPADDRESS_ROLE_CHOICES
+        choices=IPAddressRoleChoices
     )
     )
     tag = TagFilter()
     tag = TagFilter()
 
 
@@ -424,7 +424,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         label='Role (slug)',
         label='Role (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=VLAN_STATUS_CHOICES,
+        choices=VLANStatusChoices,
         null_value=None
         null_value=None
     )
     )
     tag = TagFilter()
     tag = TagFilter()

+ 3 - 3
netbox/ipam/fixtures/ipam.json

@@ -40,7 +40,7 @@
         "site": 1,
         "site": 1,
         "vrf": null,
         "vrf": null,
         "vlan": null,
         "vlan": null,
-        "status": 1,
+        "status": "active",
         "role": 1,
         "role": 1,
         "description": ""
         "description": ""
     }
     }
@@ -56,7 +56,7 @@
         "site": 1,
         "site": 1,
         "vrf": null,
         "vrf": null,
         "vlan": null,
         "vlan": null,
-        "status": 1,
+        "status": "active",
         "role": 1,
         "role": 1,
         "description": ""
         "description": ""
     }
     }
@@ -322,7 +322,7 @@
         "site": 1,
         "site": 1,
         "vid": 999,
         "vid": 999,
         "name": "TEST",
         "name": "TEST",
-        "status": 1,
+        "status": "active",
         "role": 1
         "role": 1
     }
     }
 }
 }

+ 15 - 15
netbox/ipam/forms.py

@@ -13,7 +13,7 @@ from utilities.forms import (
     StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
     StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
-from .constants import *
+from .choices import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 IP_FAMILY_CHOICES = [
 IP_FAMILY_CHOICES = [
@@ -374,7 +374,7 @@ class PrefixCSVForm(forms.ModelForm):
         required=False
         required=False
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=PREFIX_STATUS_CHOICES,
+        choices=PrefixStatusChoices,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
     role = forms.ModelChoiceField(
     role = forms.ModelChoiceField(
@@ -459,7 +459,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(PREFIX_STATUS_CHOICES),
+        choices=add_blank_choice(PrefixStatusChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -527,7 +527,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=PREFIX_STATUS_CHOICES,
+        choices=PrefixStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -764,11 +764,11 @@ class IPAddressCSVForm(forms.ModelForm):
         }
         }
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=IPADDRESS_STATUS_CHOICES,
+        choices=IPAddressStatusChoices,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
     role = CSVChoiceField(
     role = CSVChoiceField(
-        choices=IPADDRESS_ROLE_CHOICES,
+        choices=IPAddressRoleChoices,
         required=False,
         required=False,
         help_text='Functional role'
         help_text='Functional role'
     )
     )
@@ -893,12 +893,12 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(IPADDRESS_STATUS_CHOICES),
+        choices=add_blank_choice(IPAddressStatusChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     role = forms.ChoiceField(
     role = forms.ChoiceField(
-        choices=add_blank_choice(IPADDRESS_ROLE_CHOICES),
+        choices=add_blank_choice(IPAddressRoleChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -972,12 +972,12 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=IPADDRESS_STATUS_CHOICES,
+        choices=IPAddressStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
     role = forms.MultipleChoiceField(
     role = forms.MultipleChoiceField(
-        choices=IPADDRESS_ROLE_CHOICES,
+        choices=IPAddressRoleChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -1111,7 +1111,7 @@ class VLANCSVForm(forms.ModelForm):
         }
         }
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=VLAN_STATUS_CHOICES,
+        choices=VLANStatusChoices,
         help_text='Operational status'
         help_text='Operational status'
     )
     )
     role = forms.ModelChoiceField(
     role = forms.ModelChoiceField(
@@ -1180,7 +1180,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         )
         )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(VLAN_STATUS_CHOICES),
+        choices=add_blank_choice(VLANStatusChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -1229,7 +1229,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=VLAN_STATUS_CHOICES,
+        choices=VLANStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -1292,7 +1292,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         label='Search'
         label='Search'
     )
     )
     protocol = forms.ChoiceField(
     protocol = forms.ChoiceField(
-        choices=add_blank_choice(IP_PROTOCOL_CHOICES),
+        choices=add_blank_choice(ServiceProtocolChoices),
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -1307,7 +1307,7 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     protocol = forms.ChoiceField(
     protocol = forms.ChoiceField(
-        choices=add_blank_choice(IP_PROTOCOL_CHOICES),
+        choices=add_blank_choice(ServiceProtocolChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )

+ 37 - 0
netbox/ipam/migrations/0028_3569_prefix_fields.py

@@ -0,0 +1,37 @@
+from django.db import migrations, models
+
+
+PREFIX_STATUS_CHOICES = (
+    (0, 'container'),
+    (1, 'active'),
+    (2, 'reserved'),
+    (3, 'deprecated'),
+)
+
+
+def prefix_status_to_slug(apps, schema_editor):
+    Prefix = apps.get_model('ipam', 'Prefix')
+    for id, slug in PREFIX_STATUS_CHOICES:
+        Prefix.objects.filter(status=str(id)).update(status=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('ipam', '0027_ipaddress_add_dns_name'),
+    ]
+
+    operations = [
+
+        # Prefix.status
+        migrations.AlterField(
+            model_name='prefix',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=prefix_status_to_slug
+        ),
+
+    ]

+ 69 - 0
netbox/ipam/migrations/0029_3569_ipaddress_fields.py

@@ -0,0 +1,69 @@
+from django.db import migrations, models
+
+
+IPADDRESS_STATUS_CHOICES = (
+    (0, 'container'),
+    (1, 'active'),
+    (2, 'reserved'),
+    (3, 'deprecated'),
+)
+
+IPADDRESS_ROLE_CHOICES = (
+    (10, 'loopback'),
+    (20, 'secondary'),
+    (30, 'anycast'),
+    (40, 'vip'),
+    (41, 'vrrp'),
+    (42, 'hsrp'),
+    (43, 'glbp'),
+    (44, 'carp'),
+)
+
+
+def ipaddress_status_to_slug(apps, schema_editor):
+    IPAddress = apps.get_model('ipam', 'IPAddress')
+    for id, slug in IPADDRESS_STATUS_CHOICES:
+        IPAddress.objects.filter(status=str(id)).update(status=slug)
+
+
+def ipaddress_role_to_slug(apps, schema_editor):
+    IPAddress = apps.get_model('ipam', 'IPAddress')
+    for id, slug in IPADDRESS_STATUS_CHOICES:
+        IPAddress.objects.filter(role=str(id)).update(role=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('ipam', '0028_3569_prefix_fields'),
+    ]
+
+    operations = [
+
+        # IPAddress.status
+        migrations.AlterField(
+            model_name='ipaddress',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=ipaddress_status_to_slug
+        ),
+
+        # IPAddress.role
+        migrations.AlterField(
+            model_name='ipaddress',
+            name='role',
+            field=models.CharField(blank=True, default='', max_length=50),
+        ),
+        migrations.RunPython(
+            code=ipaddress_role_to_slug
+        ),
+        migrations.AlterField(
+            model_name='ipaddress',
+            name='role',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+
+    ]

+ 36 - 0
netbox/ipam/migrations/0030_3569_vlan_fields.py

@@ -0,0 +1,36 @@
+from django.db import migrations, models
+
+
+VLAN_STATUS_CHOICES = (
+    (1, 'active'),
+    (2, 'reserved'),
+    (3, 'deprecated'),
+)
+
+
+def vlan_status_to_slug(apps, schema_editor):
+    VLAN = apps.get_model('ipam', 'VLAN')
+    for id, slug in VLAN_STATUS_CHOICES:
+        VLAN.objects.filter(status=str(id)).update(status=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('ipam', '0029_3569_ipaddress_fields'),
+    ]
+
+    operations = [
+
+        # VLAN.status
+        migrations.AlterField(
+            model_name='vlan',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=vlan_status_to_slug
+        ),
+
+    ]

+ 35 - 0
netbox/ipam/migrations/0031_3569_service_fields.py

@@ -0,0 +1,35 @@
+from django.db import migrations, models
+
+
+SERVICE_PROTOCOL_CHOICES = (
+    (6, 'tcp'),
+    (17, 'udp'),
+)
+
+
+def service_protocol_to_slug(apps, schema_editor):
+    Service = apps.get_model('ipam', 'Service')
+    for id, slug in SERVICE_PROTOCOL_CHOICES:
+        Service.objects.filter(protocol=str(id)).update(protocol=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('ipam', '0030_3569_vlan_fields'),
+    ]
+
+    operations = [
+
+        # Service.protocol
+        migrations.AlterField(
+            model_name='service',
+            name='protocol',
+            field=models.CharField(max_length=50),
+        ),
+        migrations.RunPython(
+            code=service_protocol_to_slug
+        ),
+
+    ]

+ 74 - 25
netbox/ipam/models.py

@@ -14,12 +14,29 @@ from extras.models import CustomFieldModel, ObjectChange, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 from utilities.utils import serialize_object
 from utilities.utils import serialize_object
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
-from .constants import *
+from .choices import *
 from .fields import IPNetworkField, IPAddressField
 from .fields import IPNetworkField, IPAddressField
 from .querysets import PrefixQuerySet
 from .querysets import PrefixQuerySet
 from .validators import DNSValidator
 from .validators import DNSValidator
 
 
 
 
+# IP address families
+AF_CHOICES = (
+    (4, 'IPv4'),
+    (6, 'IPv6'),
+)
+
+IPADDRESS_ROLES_NONUNIQUE = (
+    # IPAddress roles which are exempt from unique address enforcement
+    IPAddressRoleChoices.ROLE_ANYCAST,
+    IPAddressRoleChoices.ROLE_VIP,
+    IPAddressRoleChoices.ROLE_VRRP,
+    IPAddressRoleChoices.ROLE_HSRP,
+    IPAddressRoleChoices.ROLE_GLBP,
+    IPAddressRoleChoices.ROLE_CARP,
+)
+
+
 class VRF(ChangeLoggedModel, CustomFieldModel):
 class VRF(ChangeLoggedModel, CustomFieldModel):
     """
     """
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@@ -297,9 +314,10 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
         null=True,
         null=True,
         verbose_name='VLAN'
         verbose_name='VLAN'
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=PREFIX_STATUS_CHOICES,
-        default=PREFIX_STATUS_ACTIVE,
+    status = models.CharField(
+        max_length=50,
+        choices=PrefixStatusChoices,
+        default=PrefixStatusChoices.STATUS_ACTIVE,
         verbose_name='Status',
         verbose_name='Status',
         help_text='Operational status of this prefix'
         help_text='Operational status of this prefix'
     )
     )
@@ -333,6 +351,13 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        'container': 'default',
+        'active': 'primary',
+        'reserved': 'info',
+        'deprecated': 'danger',
+    }
+
     class Meta:
     class Meta:
         ordering = [F('vrf').asc(nulls_first=True), 'family', 'prefix']
         ordering = [F('vrf').asc(nulls_first=True), 'family', 'prefix']
         verbose_name_plural = 'prefixes'
         verbose_name_plural = 'prefixes'
@@ -404,7 +429,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
     prefix_length = property(fset=_set_prefix_length)
     prefix_length = property(fset=_set_prefix_length)
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CHOICE_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
     def get_duplicates(self):
     def get_duplicates(self):
         return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
         return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
@@ -414,7 +439,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
         Return all Prefixes within this Prefix and VRF. If this Prefix is a container in the global table, return child
         Return all Prefixes within this Prefix and VRF. If this Prefix is a container in the global table, return child
         Prefixes belonging to any VRF.
         Prefixes belonging to any VRF.
         """
         """
-        if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER:
+        if self.vrf is None and self.status == PrefixStatusChoices.STATUS_CONTAINER:
             return Prefix.objects.filter(prefix__net_contained=str(self.prefix))
             return Prefix.objects.filter(prefix__net_contained=str(self.prefix))
         else:
         else:
             return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
             return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
@@ -424,7 +449,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
         Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return
         Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return
         child IPAddresses belonging to any VRF.
         child IPAddresses belonging to any VRF.
         """
         """
-        if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER:
+        if self.vrf is None and self.status == PrefixStatusChoices.STATUS_CONTAINER:
             return IPAddress.objects.filter(address__net_host_contained=str(self.prefix))
             return IPAddress.objects.filter(address__net_host_contained=str(self.prefix))
         else:
         else:
             return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
             return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
@@ -490,7 +515,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
         Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
         Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
         "container", calculate utilization based on child prefixes. For all others, count child IP addresses.
         "container", calculate utilization based on child prefixes. For all others, count child IP addresses.
         """
         """
-        if self.status == PREFIX_STATUS_CONTAINER:
+        if self.status == PrefixStatusChoices.STATUS_CONTAINER:
             queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
             queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
             child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
             child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
             return int(float(child_prefixes.size) / self.prefix.size * 100)
             return int(float(child_prefixes.size) / self.prefix.size * 100)
@@ -550,17 +575,16 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=IPADDRESS_STATUS_CHOICES,
-        default=IPADDRESS_STATUS_ACTIVE,
-        verbose_name='Status',
+    status = models.CharField(
+        max_length=50,
+        choices=IPAddressStatusChoices,
+        default=IPAddressStatusChoices.STATUS_ACTIVE,
         help_text='The operational status of this IP'
         help_text='The operational status of this IP'
     )
     )
-    role = models.PositiveSmallIntegerField(
-        verbose_name='Role',
-        choices=IPADDRESS_ROLE_CHOICES,
+    role = models.CharField(
+        max_length=50,
+        choices=IPAddressRoleChoices,
         blank=True,
         blank=True,
-        null=True,
         help_text='The functional role of this IP'
         help_text='The functional role of this IP'
     )
     )
     interface = models.ForeignKey(
     interface = models.ForeignKey(
@@ -604,6 +628,24 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
         'dns_name', 'description',
         'dns_name', 'description',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        'active': 'primary',
+        'reserved': 'info',
+        'deprecated': 'danger',
+        'dhcp': 'success',
+    }
+
+    ROLE_CLASS_MAP = {
+        'loopback': 'default',
+        'secondary': 'primary',
+        'anycast': 'warning',
+        'vip': 'success',
+        'vrrp': 'success',
+        'hsrp': 'success',
+        'glbp': 'success',
+        'carp': 'success',
+    }
+
     class Meta:
     class Meta:
         ordering = ['family', 'address']
         ordering = ['family', 'address']
         verbose_name = 'IP address'
         verbose_name = 'IP address'
@@ -737,10 +779,10 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
         return None
         return None
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CHOICE_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
     def get_role_class(self):
     def get_role_class(self):
-        return ROLE_CHOICE_CLASSES[self.role]
+        return self.ROLE_CLASS_MAP[self.role]
 
 
 
 
 class VLANGroup(ChangeLoggedModel):
 class VLANGroup(ChangeLoggedModel):
@@ -831,10 +873,10 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=VLAN_STATUS_CHOICES,
-        default=1,
-        verbose_name='Status'
+    status = models.CharField(
+        max_length=50,
+        choices=VLANStatusChoices,
+        default=VLANStatusChoices.STATUS_ACTIVE
     )
     )
     role = models.ForeignKey(
     role = models.ForeignKey(
         to='ipam.Role',
         to='ipam.Role',
@@ -857,6 +899,12 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
 
 
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
 
+    STATUS_CLASS_MAP = {
+        'active': 'primary',
+        'reserved': 'info',
+        'deprecated': 'danger',
+    }
+
     class Meta:
     class Meta:
         ordering = ['site', 'group', 'vid']
         ordering = ['site', 'group', 'vid']
         unique_together = [
         unique_together = [
@@ -899,7 +947,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
         return None
         return None
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return STATUS_CHOICE_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP[self.status]
 
 
     def get_members(self):
     def get_members(self):
         # Return all interfaces assigned to this VLAN
         # Return all interfaces assigned to this VLAN
@@ -932,8 +980,9 @@ class Service(ChangeLoggedModel, CustomFieldModel):
     name = models.CharField(
     name = models.CharField(
         max_length=30
         max_length=30
     )
     )
-    protocol = models.PositiveSmallIntegerField(
-        choices=IP_PROTOCOL_CHOICES
+    protocol = models.CharField(
+        max_length=50,
+        choices=ServiceProtocolChoices
     )
     )
     port = models.PositiveIntegerField(
     port = models.PositiveIntegerField(
         validators=[MinValueValidator(1), MaxValueValidator(65535)],
         validators=[MinValueValidator(1), MaxValueValidator(65535)],

+ 9 - 9
netbox/ipam/tests/test_api.py

@@ -5,7 +5,7 @@ from netaddr import IPNetwork
 from rest_framework import status
 from rest_framework import status
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
+from ipam.choices import ServiceProtocolChoices
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
 
 
@@ -996,13 +996,13 @@ class ServiceTest(APITestCase):
             name='Test Device 2', site=site, device_type=devicetype, device_role=devicerole
             name='Test Device 2', site=site, device_type=devicetype, device_role=devicerole
         )
         )
         self.service1 = Service.objects.create(
         self.service1 = Service.objects.create(
-            device=self.device1, name='Test Service 1', protocol=IP_PROTOCOL_TCP, port=1
+            device=self.device1, name='Test Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=1
         )
         )
         self.service1 = Service.objects.create(
         self.service1 = Service.objects.create(
-            device=self.device1, name='Test Service 2', protocol=IP_PROTOCOL_TCP, port=2
+            device=self.device1, name='Test Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=2
         )
         )
         self.service1 = Service.objects.create(
         self.service1 = Service.objects.create(
-            device=self.device1, name='Test Service 3', protocol=IP_PROTOCOL_TCP, port=3
+            device=self.device1, name='Test Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=3
         )
         )
 
 
     def test_get_service(self):
     def test_get_service(self):
@@ -1024,7 +1024,7 @@ class ServiceTest(APITestCase):
         data = {
         data = {
             'device': self.device1.pk,
             'device': self.device1.pk,
             'name': 'Test Service 4',
             'name': 'Test Service 4',
-            'protocol': IP_PROTOCOL_TCP,
+            'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
             'port': 4,
             'port': 4,
         }
         }
 
 
@@ -1045,19 +1045,19 @@ class ServiceTest(APITestCase):
             {
             {
                 'device': self.device1.pk,
                 'device': self.device1.pk,
                 'name': 'Test Service 4',
                 'name': 'Test Service 4',
-                'protocol': IP_PROTOCOL_TCP,
+                'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
                 'port': 4,
                 'port': 4,
             },
             },
             {
             {
                 'device': self.device1.pk,
                 'device': self.device1.pk,
                 'name': 'Test Service 5',
                 'name': 'Test Service 5',
-                'protocol': IP_PROTOCOL_TCP,
+                'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
                 'port': 5,
                 'port': 5,
             },
             },
             {
             {
                 'device': self.device1.pk,
                 'device': self.device1.pk,
                 'name': 'Test Service 6',
                 'name': 'Test Service 6',
-                'protocol': IP_PROTOCOL_TCP,
+                'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
                 'port': 6,
                 'port': 6,
             },
             },
         ]
         ]
@@ -1076,7 +1076,7 @@ class ServiceTest(APITestCase):
         data = {
         data = {
             'device': self.device2.pk,
             'device': self.device2.pk,
             'name': 'Test Service X',
             'name': 'Test Service X',
-            'protocol': IP_PROTOCOL_UDP,
+            'protocol': ServiceProtocolChoices.PROTOCOL_UDP,
             'port': 99,
             'port': 99,
         }
         }
 
 

+ 3 - 3
netbox/ipam/tests/test_models.py

@@ -2,7 +2,7 @@ import netaddr
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.test import TestCase, override_settings
 from django.test import TestCase, override_settings
 
 
-from ipam.constants import IPADDRESS_ROLE_VIP
+from ipam.choices import IPAddressRoleChoices
 from ipam.models import IPAddress, Prefix, VRF
 from ipam.models import IPAddress, Prefix, VRF
 
 
 
 
@@ -61,5 +61,5 @@ class TestIPAddress(TestCase):
 
 
     @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
     @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
     def test_duplicate_nonunique_role(self):
     def test_duplicate_nonunique_role(self):
-        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPADDRESS_ROLE_VIP)
-        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPADDRESS_ROLE_VIP)
+        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
+        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)

+ 4 - 4
netbox/ipam/tests/test_views.py

@@ -5,7 +5,7 @@ from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.constants import IP_PROTOCOL_TCP
+from ipam.choices import ServiceProtocolChoices
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.testing import create_test_user
 from utilities.testing import create_test_user
 
 
@@ -264,9 +264,9 @@ class ServiceTestCase(TestCase):
         device.save()
         device.save()
 
 
         Service.objects.bulk_create([
         Service.objects.bulk_create([
-            Service(device=device, name='Service 1', protocol=IP_PROTOCOL_TCP, port=101),
-            Service(device=device, name='Service 2', protocol=IP_PROTOCOL_TCP, port=102),
-            Service(device=device, name='Service 3', protocol=IP_PROTOCOL_TCP, port=103),
+            Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101),
+            Service(device=device, name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=102),
+            Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103),
         ])
         ])
 
 
     def test_service_list(self):
     def test_service_list(self):

+ 6 - 6
netbox/ipam/views.py

@@ -14,7 +14,7 @@ from utilities.views import (
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
-from .constants import *
+from .choices import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
@@ -217,13 +217,13 @@ class RIRListView(PermissionRequiredMixin, ObjectListView):
 
 
                 # Find all consumed space for each prefix status (we ignore containers for this purpose).
                 # Find all consumed space for each prefix status (we ignore containers for this purpose).
                 active_prefixes = netaddr.cidr_merge(
                 active_prefixes = netaddr.cidr_merge(
-                    [p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)]
+                    [p.prefix for p in queryset.filter(status=PrefixStatusChoices.STATUS_ACTIVE)]
                 )
                 )
                 reserved_prefixes = netaddr.cidr_merge(
                 reserved_prefixes = netaddr.cidr_merge(
-                    [p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)]
+                    [p.prefix for p in queryset.filter(status=PrefixStatusChoices.STATUS_RESERVED)]
                 )
                 )
                 deprecated_prefixes = netaddr.cidr_merge(
                 deprecated_prefixes = netaddr.cidr_merge(
-                    [p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)]
+                    [p.prefix for p in queryset.filter(status=PrefixStatusChoices.STATUS_DEPRECATED)]
                 )
                 )
 
 
                 # Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
                 # Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
@@ -665,8 +665,8 @@ class IPAddressView(PermissionRequiredMixin, View):
             'nat_inside', 'interface__device'
             'nat_inside', 'interface__device'
         )
         )
         # Exclude anycast IPs if this IP is anycast
         # Exclude anycast IPs if this IP is anycast
-        if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
-            duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST)
+        if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
+            duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
         duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
         duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
 
 
         # Related IP table
         # Related IP table

+ 14 - 1
netbox/utilities/api.py

@@ -13,6 +13,7 @@ from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
 
 
+from utilities.choices import ChoiceSet
 from .utils import dict_to_filter_params, dynamic_import
 from .utils import dict_to_filter_params, dynamic_import
 
 
 
 
@@ -64,14 +65,17 @@ class ChoiceField(Field):
     Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
     Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
     """
     """
     def __init__(self, choices, **kwargs):
     def __init__(self, choices, **kwargs):
+        self.choiceset = choices
         self._choices = dict()
         self._choices = dict()
+
+        # Unpack grouped choices
         for k, v in choices:
         for k, v in choices:
-            # Unpack grouped choices
             if type(v) in [list, tuple]:
             if type(v) in [list, tuple]:
                 for k2, v2 in v:
                 for k2, v2 in v:
                     self._choices[k2] = v2
                     self._choices[k2] = v2
             else:
             else:
                 self._choices[k] = v
                 self._choices[k] = v
+
         super().__init__(**kwargs)
         super().__init__(**kwargs)
 
 
     def to_representation(self, obj):
     def to_representation(self, obj):
@@ -81,6 +85,11 @@ class ChoiceField(Field):
             ('value', obj),
             ('value', obj),
             ('label', self._choices[obj])
             ('label', self._choices[obj])
         ])
         ])
+
+        # Include legacy numeric ID (where applicable)
+        if type(self.choiceset) is ChoiceSet and obj in self.choiceset.LEGACY_MAP:
+            data['id'] = self.choiceset.LEGACY_MAP.get(obj)
+
         return data
         return data
 
 
     def to_internal_value(self, data):
     def to_internal_value(self, data):
@@ -104,6 +113,10 @@ class ChoiceField(Field):
         try:
         try:
             if data in self._choices:
             if data in self._choices:
                 return data
                 return data
+            # Check if data is a legacy numeric ID
+            slug = self.choiceset.id_to_slug(data)
+            if slug is not None:
+                return slug
         except TypeError:  # Input is an unhashable type
         except TypeError:  # Input is an unhashable type
             pass
             pass
 
 

+ 36 - 0
netbox/utilities/choices.py

@@ -0,0 +1,36 @@
+class ChoiceSetMeta(type):
+    """
+    Metaclass for ChoiceSet
+    """
+    def __call__(cls, *args, **kwargs):
+        # Django will check if a 'choices' value is callable, and if so assume that it returns an iterable
+        return getattr(cls, 'CHOICES', ())
+
+    def __iter__(cls):
+        choices = getattr(cls, 'CHOICES', ())
+        return iter(choices)
+
+
+class ChoiceSet(metaclass=ChoiceSetMeta):
+
+    CHOICES = list()
+    LEGACY_MAP = dict()
+
+    @classmethod
+    def slug_to_id(cls, slug):
+        """
+        Return the legacy integer value corresponding to a slug.
+        """
+        return cls.LEGACY_MAP.get(slug)
+
+    @classmethod
+    def id_to_slug(cls, legacy_id):
+        """
+        Return the slug value corresponding to a legacy integer value.
+        """
+        if legacy_id in cls.LEGACY_MAP.values():
+            # Invert the legacy map to allow lookup by integer
+            legacy_map = dict([
+                (id, slug) for slug, id in cls.LEGACY_MAP.items()
+            ])
+            return legacy_map.get(legacy_id)

+ 12 - 6
netbox/utilities/utils.py

@@ -5,7 +5,7 @@ from collections import OrderedDict
 from django.core.serializers import serialize
 from django.core.serializers import serialize
 from django.db.models import Count, OuterRef, Subquery
 from django.db.models import Count, OuterRef, Subquery
 
 
-from dcim.constants import LENGTH_UNIT_CENTIMETER, LENGTH_UNIT_FOOT, LENGTH_UNIT_INCH, LENGTH_UNIT_METER
+from dcim.choices import CableLengthUnitChoices
 
 
 
 
 def csv_format(data):
 def csv_format(data):
@@ -165,12 +165,18 @@ def to_meters(length, unit):
     length = int(length)
     length = int(length)
     if length < 0:
     if length < 0:
         raise ValueError("Length must be a positive integer")
         raise ValueError("Length must be a positive integer")
-    if unit == LENGTH_UNIT_METER:
+
+    valid_units = [u[0] for u in CableLengthUnitChoices]
+    if unit not in valid_units:
+        raise ValueError(
+            "Unknown unit {}. Must be one of the following: {}".format(unit, ', '.join(valid_units))
+        )
+
+    if unit == CableLengthUnitChoices.UNIT_METER:
         return length
         return length
-    if unit == LENGTH_UNIT_CENTIMETER:
+    if unit == CableLengthUnitChoices.UNIT_CENTIMETER:
         return length / 100
         return length / 100
-    if unit == LENGTH_UNIT_FOOT:
+    if unit == CableLengthUnitChoices.UNIT_FOOT:
         return length * 0.3048
         return length * 0.3048
-    if unit == LENGTH_UNIT_INCH:
+    if unit == CableLengthUnitChoices.UNIT_INCH:
         return length * 0.3048 * 12
         return length * 0.3048 * 12
-    raise ValueError("Unknown unit {}. Must be 'm', 'cm', 'ft', or 'in'.".format(unit))

+ 5 - 5
netbox/virtualization/api/serializers.py

@@ -3,14 +3,14 @@ from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 
 from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
-from dcim.constants import IFACE_TYPE_CHOICES, IFACE_TYPE_VIRTUAL, IFACE_MODE_CHOICES
+from dcim.choices import InterfaceModeChoices, InterfaceTypeChoices
 from dcim.models import Interface
 from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
 from ipam.models import VLAN
 from ipam.models import VLAN
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
 from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
-from virtualization.constants import VM_STATUS_CHOICES
+from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .nested_serializers import *
 from .nested_serializers import *
 
 
@@ -57,7 +57,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
 #
 #
 
 
 class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
-    status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
     site = NestedSiteSerializer(read_only=True)
     site = NestedSiteSerializer(read_only=True)
     cluster = NestedClusterSerializer()
     cluster = NestedClusterSerializer()
     role = NestedDeviceRoleSerializer(required=False, allow_null=True)
     role = NestedDeviceRoleSerializer(required=False, allow_null=True)
@@ -98,8 +98,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
 
 
 class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
 class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
     virtual_machine = NestedVirtualMachineSerializer()
     virtual_machine = NestedVirtualMachineSerializer()
-    type = ChoiceField(choices=IFACE_TYPE_CHOICES, default=IFACE_TYPE_VIRTUAL, required=False)
-    mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
+    type = ChoiceField(choices=InterfaceTypeChoices, default=InterfaceTypeChoices.TYPE_VIRTUAL, required=False)
+    mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     tagged_vlans = SerializedPKRelatedField(
     tagged_vlans = SerializedPKRelatedField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),

+ 24 - 0
netbox/virtualization/choices.py

@@ -0,0 +1,24 @@
+from utilities.choices import ChoiceSet
+
+
+#
+# VirtualMachines
+#
+
+class VirtualMachineStatusChoices(ChoiceSet):
+
+    STATUS_ACTIVE = 'active'
+    STATUS_OFFLINE = 'offline'
+    STATUS_STAGED = 'staged'
+
+    CHOICES = (
+        (STATUS_ACTIVE, 'Active'),
+        (STATUS_OFFLINE, 'Offline'),
+        (STATUS_STAGED, 'Staged'),
+    )
+
+    LEGACY_MAP = {
+        STATUS_OFFLINE: 0,
+        STATUS_ACTIVE: 1,
+        STATUS_STAGED: 3,
+    }

+ 0 - 15
netbox/virtualization/constants.py

@@ -1,15 +0,0 @@
-from dcim.constants import DEVICE_STATUS_ACTIVE, DEVICE_STATUS_OFFLINE, DEVICE_STATUS_STAGED
-
-# VirtualMachine statuses (replicated from Device statuses)
-VM_STATUS_CHOICES = [
-    [DEVICE_STATUS_ACTIVE, 'Active'],
-    [DEVICE_STATUS_OFFLINE, 'Offline'],
-    [DEVICE_STATUS_STAGED, 'Staged'],
-]
-
-# Bootstrap CSS classes for VirtualMachine statuses
-VM_STATUS_CLASSES = {
-    0: 'warning',
-    1: 'success',
-    3: 'primary',
-}

+ 2 - 2
netbox/virtualization/filters.py

@@ -10,7 +10,7 @@ from tenancy.models import Tenant
 from utilities.filters import (
 from utilities.filters import (
     MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
     MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
 )
 )
-from .constants import *
+from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 
 
@@ -96,7 +96,7 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdate
         label='Search',
         label='Search',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=VM_STATUS_CHOICES,
+        choices=VirtualMachineStatusChoices,
         null_value=None
         null_value=None
     )
     )
     cluster_group_id = django_filters.ModelMultipleChoiceFilter(
     cluster_group_id = django_filters.ModelMultipleChoiceFilter(

+ 12 - 12
netbox/virtualization/forms.py

@@ -2,7 +2,7 @@ from django import forms
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from taggit.forms import TagField
 from taggit.forms import TagField
 
 
-from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL, IFACE_MODE_CHOICES
+from dcim.choices import InterfaceModeChoices, InterfaceTypeChoices
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
 from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
 from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
 from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
 from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
@@ -15,11 +15,11 @@ from utilities.forms import (
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
     SmallTextarea, StaticSelect2, StaticSelect2Multiple
     SmallTextarea, StaticSelect2, StaticSelect2Multiple
 )
 )
-from .constants import *
+from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 VIFACE_TYPE_CHOICES = (
 VIFACE_TYPE_CHOICES = (
-    (IFACE_TYPE_VIRTUAL, 'Virtual'),
+    (InterfaceTypeChoices.TYPE_VIRTUAL, 'Virtual'),
 )
 )
 
 
 
 
@@ -428,7 +428,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
 
 
 class VirtualMachineCSVForm(forms.ModelForm):
 class VirtualMachineCSVForm(forms.ModelForm):
     status = CSVChoiceField(
     status = CSVChoiceField(
-        choices=VM_STATUS_CHOICES,
+        choices=VirtualMachineStatusChoices,
         required=False,
         required=False,
         help_text='Operational status of device'
         help_text='Operational status of device'
     )
     )
@@ -481,7 +481,7 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=add_blank_choice(VM_STATUS_CHOICES),
+        choices=add_blank_choice(VirtualMachineStatusChoices),
         required=False,
         required=False,
         initial='',
         initial='',
         widget=StaticSelect2(),
         widget=StaticSelect2(),
@@ -612,7 +612,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
         )
         )
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
-        choices=VM_STATUS_CHOICES,
+        choices=VirtualMachineStatusChoices,
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
@@ -717,13 +717,13 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         tagged_vlans = self.cleaned_data['tagged_vlans']
         tagged_vlans = self.cleaned_data['tagged_vlans']
 
 
         # Untagged interfaces cannot be assigned tagged VLANs
         # Untagged interfaces cannot be assigned tagged VLANs
-        if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
+        if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
             raise forms.ValidationError({
             raise forms.ValidationError({
                 'mode': "An access interface cannot have tagged VLANs assigned."
                 'mode': "An access interface cannot have tagged VLANs assigned."
             })
             })
 
 
         # Remove all tagged VLAN assignments from "tagged all" interfaces
         # Remove all tagged VLAN assignments from "tagged all" interfaces
-        elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
+        elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
             self.cleaned_data['tagged_vlans'] = []
             self.cleaned_data['tagged_vlans'] = []
 
 
 
 
@@ -733,7 +733,7 @@ class InterfaceCreateForm(ComponentForm):
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=VIFACE_TYPE_CHOICES,
         choices=VIFACE_TYPE_CHOICES,
-        initial=IFACE_TYPE_VIRTUAL,
+        initial=InterfaceTypeChoices.TYPE_VIRTUAL,
         widget=forms.HiddenInput()
         widget=forms.HiddenInput()
     )
     )
     enabled = forms.BooleanField(
     enabled = forms.BooleanField(
@@ -754,7 +754,7 @@ class InterfaceCreateForm(ComponentForm):
         required=False
         required=False
     )
     )
     mode = forms.ChoiceField(
     mode = forms.ChoiceField(
-        choices=add_blank_choice(IFACE_MODE_CHOICES),
+        choices=add_blank_choice(InterfaceModeChoices),
         required=False,
         required=False,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
@@ -839,7 +839,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False
         required=False
     )
     )
     mode = forms.ChoiceField(
     mode = forms.ChoiceField(
-        choices=add_blank_choice(IFACE_MODE_CHOICES),
+        choices=add_blank_choice(InterfaceModeChoices),
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -918,7 +918,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
 class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
 class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=VIFACE_TYPE_CHOICES,
         choices=VIFACE_TYPE_CHOICES,
-        initial=IFACE_TYPE_VIRTUAL,
+        initial=InterfaceTypeChoices.TYPE_VIRTUAL,
         widget=forms.HiddenInput()
         widget=forms.HiddenInput()
     )
     )
     enabled = forms.BooleanField(
     enabled = forms.BooleanField(

+ 36 - 0
netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py

@@ -0,0 +1,36 @@
+from django.db import migrations, models
+
+
+VIRTUALMACHINE_STATUS_CHOICES = (
+    (0, 'offline'),
+    (1, 'active'),
+    (3, 'staged'),
+)
+
+
+def virtualmachine_status_to_slug(apps, schema_editor):
+    VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
+    for id, slug in VIRTUALMACHINE_STATUS_CHOICES:
+        VirtualMachine.objects.filter(status=str(id)).update(status=slug)
+
+
+class Migration(migrations.Migration):
+    atomic = False
+
+    dependencies = [
+        ('virtualization', '0010_cluster_add_tenant'),
+    ]
+
+    operations = [
+
+        # VirtualMachine.status
+        migrations.AlterField(
+            model_name='virtualmachine',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+        migrations.RunPython(
+            code=virtualmachine_status_to_slug
+        ),
+
+    ]

+ 12 - 5
netbox/virtualization/models.py

@@ -8,7 +8,7 @@ from taggit.managers import TaggableManager
 from dcim.models import Device
 from dcim.models import Device
 from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
 from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
-from .constants import *
+from .choices import *
 
 
 
 
 #
 #
@@ -193,9 +193,10 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         max_length=64,
         max_length=64,
         unique=True
         unique=True
     )
     )
-    status = models.PositiveSmallIntegerField(
-        choices=VM_STATUS_CHOICES,
-        default=DEVICE_STATUS_ACTIVE,
+    status = models.CharField(
+        max_length=50,
+        choices=VirtualMachineStatusChoices,
+        default=VirtualMachineStatusChoices.STATUS_ACTIVE,
         verbose_name='Status'
         verbose_name='Status'
     )
     )
     role = models.ForeignKey(
     role = models.ForeignKey(
@@ -252,6 +253,12 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
     ]
     ]
 
 
+    STATUS_CLASS_MAP = {
+        'active': 'success',
+        'offline': 'warning',
+        'staged': 'primary',
+    }
+
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
@@ -294,7 +301,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         )
         )
 
 
     def get_status_class(self):
     def get_status_class(self):
-        return VM_STATUS_CLASSES[self.status]
+        return self.STATUS_CLASS_MAP.get(self.status)
 
 
     @property
     @property
     def primary_ip(self):
     def primary_ip(self):

+ 8 - 8
netbox/virtualization/tests/test_api.py

@@ -2,7 +2,7 @@ from django.urls import reverse
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 from rest_framework import status
 from rest_framework import status
 
 
-from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_TAGGED
+from dcim.choices import InterfaceModeChoices, InterfaceTypeChoices
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import IPAddress, VLAN
 from ipam.models import IPAddress, VLAN
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
@@ -489,17 +489,17 @@ class InterfaceTest(APITestCase):
         self.interface1 = Interface.objects.create(
         self.interface1 = Interface.objects.create(
             virtual_machine=self.virtualmachine,
             virtual_machine=self.virtualmachine,
             name='Test Interface 1',
             name='Test Interface 1',
-            type=IFACE_TYPE_VIRTUAL
+            type=InterfaceTypeChoices.TYPE_VIRTUAL
         )
         )
         self.interface2 = Interface.objects.create(
         self.interface2 = Interface.objects.create(
             virtual_machine=self.virtualmachine,
             virtual_machine=self.virtualmachine,
             name='Test Interface 2',
             name='Test Interface 2',
-            type=IFACE_TYPE_VIRTUAL
+            type=InterfaceTypeChoices.TYPE_VIRTUAL
         )
         )
         self.interface3 = Interface.objects.create(
         self.interface3 = Interface.objects.create(
             virtual_machine=self.virtualmachine,
             virtual_machine=self.virtualmachine,
             name='Test Interface 3',
             name='Test Interface 3',
-            type=IFACE_TYPE_VIRTUAL
+            type=InterfaceTypeChoices.TYPE_VIRTUAL
         )
         )
 
 
         self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
         self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
@@ -551,7 +551,7 @@ class InterfaceTest(APITestCase):
         data = {
         data = {
             'virtual_machine': self.virtualmachine.pk,
             'virtual_machine': self.virtualmachine.pk,
             'name': 'Test Interface 4',
             'name': 'Test Interface 4',
-            'mode': IFACE_MODE_TAGGED,
+            'mode': InterfaceModeChoices.MODE_TAGGED,
             'untagged_vlan': self.vlan3.id,
             'untagged_vlan': self.vlan3.id,
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
         }
         }
@@ -598,21 +598,21 @@ class InterfaceTest(APITestCase):
             {
             {
                 'virtual_machine': self.virtualmachine.pk,
                 'virtual_machine': self.virtualmachine.pk,
                 'name': 'Test Interface 4',
                 'name': 'Test Interface 4',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'virtual_machine': self.virtualmachine.pk,
                 'virtual_machine': self.virtualmachine.pk,
                 'name': 'Test Interface 5',
                 'name': 'Test Interface 5',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'virtual_machine': self.virtualmachine.pk,
                 'virtual_machine': self.virtualmachine.pk,
                 'name': 'Test Interface 6',
                 'name': 'Test Interface 6',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },