diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index 39a0b6b26..fb63654b1 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -1,7 +1,7 @@
from rest_framework import serializers
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 dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer
@@ -41,7 +41,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
provider = NestedProviderSerializer()
- status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
+ status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py
new file mode 100644
index 000000000..94a765d11
--- /dev/null
+++ b/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')
+ )
diff --git a/netbox/circuits/constants.py b/netbox/circuits/constants.py
deleted file mode 100644
index 9e180e655..000000000
--- a/netbox/circuits/constants.py
+++ /dev/null
@@ -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'),
-)
diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py
index 502d2d103..3f3f974b7 100644
--- a/netbox/circuits/filters.py
+++ b/netbox/circuits/filters.py
@@ -5,7 +5,7 @@ from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
-from .constants import *
+from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -84,7 +84,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
label='Circuit type (slug)',
)
status = django_filters.MultipleChoiceFilter(
- choices=CIRCUIT_STATUS_CHOICES,
+ choices=CircuitStatusChoices,
null_value=None
)
site_id = django_filters.ModelMultipleChoiceFilter(
diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py
index dfe4f46e4..ad99dd40d 100644
--- a/netbox/circuits/forms.py
+++ b/netbox/circuits/forms.py
@@ -9,7 +9,7 @@ from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
)
-from .constants import *
+from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -194,7 +194,7 @@ class CircuitCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
- choices=CIRCUIT_STATUS_CHOICES,
+ choices=CircuitStatusChoices,
required=False,
help_text='Operational status'
)
@@ -235,7 +235,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
+ choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -292,7 +292,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
)
)
status = forms.MultipleChoiceField(
- choices=CIRCUIT_STATUS_CHOICES,
+ choices=CircuitStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
diff --git a/netbox/circuits/migrations/0016_3569_circuit_fields.py b/netbox/circuits/migrations/0016_3569_circuit_fields.py
new file mode 100644
index 000000000..a65f72d61
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py
index 8cf18617c..5f80a4bfe 100644
--- a/netbox/circuits/models.py
+++ b/netbox/circuits/models.py
@@ -3,13 +3,13 @@ from django.db import models
from django.urls import reverse
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.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
-from .constants import *
+from .choices import *
class Provider(ChangeLoggedModel, CustomFieldModel):
@@ -132,9 +132,10 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
on_delete=models.PROTECT,
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(
to='tenancy.Tenant',
@@ -171,6 +172,15 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
'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:
ordering = ['provider', 'cid']
unique_together = ['provider', 'cid']
@@ -195,7 +205,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
)
def get_status_class(self):
- return STATUS_CLASSES[self.status]
+ return self.STATUS_CLASS_MAP.get(self.status)
def _get_termination(self, side):
for ct in self.terminations.all():
@@ -220,7 +230,7 @@ class CircuitTermination(CableTermination):
)
term_side = models.CharField(
max_length=1,
- choices=TERM_SIDE_CHOICES,
+ choices=CircuitTerminationSideChoices,
verbose_name='Termination'
)
site = models.ForeignKey(
diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py
index e53c2c402..c6100d825 100644
--- a/netbox/circuits/tests/test_api.py
+++ b/netbox/circuits/tests/test_api.py
@@ -1,9 +1,9 @@
from django.urls import reverse
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 dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
+from dcim.models import Site
from extras.constants import GRAPH_TYPE_PROVIDER
from extras.models import Graph
from utilities.testing import APITestCase
@@ -250,7 +250,7 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
- 'status': CIRCUIT_STATUS_ACTIVE,
+ 'status': CircuitStatusChoices.STATUS_ACTIVE,
}
url = reverse('circuits-api:circuit-list')
@@ -270,19 +270,19 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
- 'status': CIRCUIT_STATUS_ACTIVE,
+ 'status': CircuitStatusChoices.STATUS_ACTIVE,
},
{
'cid': 'TEST0005',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
- 'status': CIRCUIT_STATUS_ACTIVE,
+ 'status': CircuitStatusChoices.STATUS_ACTIVE,
},
{
'cid': 'TEST0006',
'provider': self.provider1.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.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
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(
- 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(
- 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(
- 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):
@@ -366,7 +378,7 @@ class CircuitTerminationTest(APITestCase):
data = {
'circuit': self.circuit3.pk,
- 'term_side': TERM_SIDE_A,
+ 'term_side': CircuitTerminationSideChoices.SIDE_A,
'site': self.site1.pk,
'port_speed': 1000000,
}
@@ -385,12 +397,15 @@ class CircuitTerminationTest(APITestCase):
def test_update_circuittermination(self):
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 = {
'circuit': self.circuit3.pk,
- 'term_side': TERM_SIDE_Z,
+ 'term_side': CircuitTerminationSideChoices.SIDE_Z,
'site': self.site2.pk,
'port_speed': 1000000,
}
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index 655b714d7..31b167a65 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -12,7 +12,7 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
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
@@ -151,12 +151,12 @@ class CircuitView(PermissionRequiredMixin, View):
termination_a = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device'
).filter(
- circuit=circuit, term_side=TERM_SIDE_A
+ circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
termination_z = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device'
).filter(
- circuit=circuit, term_side=TERM_SIDE_Z
+ circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
return render(request, 'circuits/circuit.html', {
@@ -212,8 +212,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
def circuit_terminations_swap(request, 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:
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
return redirect('circuits:circuit', pk=circuit.pk)
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 95ee15bbe..3c4170373 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -68,7 +68,7 @@ class RegionSerializer(serializers.ModelSerializer):
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)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
@@ -115,11 +115,11 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
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)
- 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)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True)
@@ -187,7 +187,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
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)
device_count = serializers.IntegerField(read_only=True)
@@ -202,7 +202,7 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
required=False
)
@@ -214,7 +214,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
required=False
)
@@ -226,7 +226,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class PowerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
- choices=PowerPortTypes.CHOICES,
+ choices=PowerPortTypeChoices,
required=False
)
@@ -238,14 +238,14 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
- choices=PowerOutletTypes.CHOICES,
+ choices=PowerOutletTypeChoices,
required=False
)
power_port = PowerPortTemplateSerializer(
required=False
)
feed_leg = ChoiceField(
- choices=POWERFEED_LEG_CHOICES,
+ choices=PowerOutletFeedLegChoices,
required=False,
allow_null=True
)
@@ -257,7 +257,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
- type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
+ type = ChoiceField(choices=InterfaceTypeChoices, required=False)
class Meta:
model = InterfaceTemplate
@@ -266,7 +266,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
class RearPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
- type = ChoiceField(choices=PORT_TYPE_CHOICES)
+ type = ChoiceField(choices=PortTypeChoices)
class Meta:
model = RearPortTemplate
@@ -275,7 +275,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class FrontPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
- type = ChoiceField(choices=PORT_TYPE_CHOICES)
+ type = ChoiceField(choices=PortTypeChoices)
rear_port = NestedRearPortTemplateSerializer()
class Meta:
@@ -324,8 +324,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer()
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_ip4 = 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):
device = NestedDeviceSerializer()
type = ChoiceField(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True)
@@ -405,7 +405,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True)
@@ -422,14 +422,14 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
- choices=PowerOutletTypes.CHOICES,
+ choices=PowerOutletTypeChoices,
required=False
)
power_port = NestedPowerPortSerializer(
required=False
)
feed_leg = ChoiceField(
- choices=POWERFEED_LEG_CHOICES,
+ choices=PowerOutletFeedLegChoices,
required=False,
allow_null=True
)
@@ -451,7 +451,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
- choices=PowerPortTypes.CHOICES,
+ choices=PowerPortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True)
@@ -467,9 +467,9 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
- type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
+ type = ChoiceField(choices=InterfaceTypeChoices, required=False)
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)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
@@ -511,7 +511,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
- type = ChoiceField(choices=PORT_TYPE_CHOICES)
+ type = ChoiceField(choices=PortTypeChoices)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
@@ -533,7 +533,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
- type = ChoiceField(choices=PORT_TYPE_CHOICES)
+ type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
@@ -586,7 +586,7 @@ class CableSerializer(ValidatedModelSerializer):
termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True)
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:
model = Cable
@@ -691,20 +691,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
default=None
)
type = ChoiceField(
- choices=POWERFEED_TYPE_CHOICES,
- default=POWERFEED_TYPE_PRIMARY
+ choices=PowerFeedTypeChoices,
+ default=PowerFeedTypeChoices.TYPE_PRIMARY
)
status = ChoiceField(
- choices=POWERFEED_STATUS_CHOICES,
- default=POWERFEED_STATUS_ACTIVE
+ choices=PowerFeedStatusChoices,
+ default=PowerFeedStatusChoices.STATUS_ACTIVE
)
supply = ChoiceField(
- choices=POWERFEED_SUPPLY_CHOICES,
- default=POWERFEED_SUPPLY_AC
+ choices=PowerFeedSupplyChoices,
+ default=PowerFeedSupplyChoices.SUPPLY_AC
)
phase = ChoiceField(
- choices=POWERFEED_PHASE_CHOICES,
- default=POWERFEED_PHASE_SINGLE
+ choices=PowerFeedPhaseChoices,
+ default=PowerFeedPhaseChoices.PHASE_SINGLE
)
tags = TagListSerializerField(
required=False
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index c9637965b..ccffa4379 100644
--- a/netbox/dcim/choices.py
+++ b/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_DB25 = 'db-25'
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
# IEC 60320
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
# IEC 60320
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
TYPE_VIRTUAL = 'virtual'
TYPE_LAG = 'lag'
@@ -315,7 +507,7 @@ class InterfaceTypes:
# Other
TYPE_OTHER = 'other'
- TYPE_CHOICES = (
+ CHOICES = (
(
'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_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc'
@@ -545,7 +749,7 @@ class PortTypes:
TYPE_LSH = 'lsh'
TYPE_LSH_APC = 'lsh-apc'
- TYPE_CHOICES = (
+ CHOICES = (
(
'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,
+ }
diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
index 2e2285b14..8dacd68f5 100644
--- a/netbox/dcim/constants.py
+++ b/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 = [
- IFACE_TYPE_VIRTUAL,
- IFACE_TYPE_LAG,
+ InterfaceTypeChoices.TYPE_VIRTUAL,
+ InterfaceTypeChoices.TYPE_LAG,
]
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
-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
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
@@ -390,56 +34,6 @@ CABLE_TERMINATION_TYPES = [
'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 = {
# (API endpoint, human-friendly name)
'consoleport': ('console-ports', 'Console port'),
@@ -461,57 +55,3 @@ COMPATIBLE_TERMINATION_TYPES = {
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'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'),
-)
diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py
index 8971f6ac7..85a1b4cbc 100644
--- a/netbox/dcim/filters.py
+++ b/netbox/dcim/filters.py
@@ -49,7 +49,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Search',
)
status = django_filters.MultipleChoiceFilter(
- choices=SITE_STATUS_CHOICES,
+ choices=SiteStatusChoices,
null_value=None
)
region_id = TreeNodeMultipleChoiceFilter(
@@ -147,7 +147,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Group',
)
status = django_filters.MultipleChoiceFilter(
- choices=RACK_STATUS_CHOICES,
+ choices=RackStatusChoices,
null_value=None
)
role_id = django_filters.ModelMultipleChoiceFilter(
@@ -511,7 +511,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
label='Device model (slug)',
)
status = django_filters.MultipleChoiceFilter(
- choices=DEVICE_STATUS_CHOICES,
+ choices=DeviceStatusChoices,
null_value=None
)
is_full_depth = django_filters.BooleanFilter(
@@ -663,7 +663,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
class ConsolePortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
@@ -679,7 +679,7 @@ class ConsolePortFilter(DeviceComponentFilterSet):
class ConsoleServerPortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
@@ -695,7 +695,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
class PowerPortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
- choices=PowerPortTypes.CHOICES,
+ choices=PowerPortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
@@ -711,7 +711,7 @@ class PowerPortFilter(DeviceComponentFilterSet):
class PowerOutletFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
- choices=PowerOutletTypes.CHOICES,
+ choices=PowerOutletTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
@@ -789,7 +789,7 @@ class InterfaceFilter(django_filters.FilterSet):
label='Assigned VID'
)
type = django_filters.MultipleChoiceFilter(
- choices=IFACE_TYPE_CHOICES,
+ choices=InterfaceTypeChoices,
null_value=None
)
@@ -980,7 +980,7 @@ class CableFilter(django_filters.FilterSet):
label='Search',
)
type = django_filters.MultipleChoiceFilter(
- choices=CABLE_TYPE_CHOICES
+ choices=CableTypeChoices
)
status = django_filters.MultipleChoiceFilter(
choices=CONNECTION_STATUS_CHOICES
diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json
index ece19a83c..b9f41edb5 100644
--- a/netbox/dcim/fixtures/dcim.json
+++ b/netbox/dcim/fixtures/dcim.json
@@ -1910,7 +1910,7 @@
"site": 1,
"rack": 1,
"position": 1,
- "face": 0,
+ "face": "front",
"status": true,
"primary_ip4": 1,
"primary_ip6": null,
@@ -1931,7 +1931,7 @@
"site": 1,
"rack": 1,
"position": 17,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": 5,
"primary_ip6": null,
@@ -1952,7 +1952,7 @@
"site": 1,
"rack": 1,
"position": 33,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -1973,7 +1973,7 @@
"site": 1,
"rack": 1,
"position": 34,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -1994,7 +1994,7 @@
"site": 1,
"rack": 2,
"position": 34,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2015,7 +2015,7 @@
"site": 1,
"rack": 2,
"position": 33,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2036,7 +2036,7 @@
"site": 1,
"rack": 2,
"position": 1,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": 3,
"primary_ip6": null,
@@ -2057,7 +2057,7 @@
"site": 1,
"rack": 2,
"position": 17,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": 19,
"primary_ip6": null,
@@ -2078,7 +2078,7 @@
"site": 1,
"rack": 1,
"position": 42,
- "face": 0,
+ "face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2099,7 +2099,7 @@
"site": 1,
"rack": 1,
"position": null,
- "face": null,
+ "face": "",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2120,7 +2120,7 @@
"site": 1,
"rack": 2,
"position": null,
- "face": null,
+ "face": "",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index 58c88ec63..9a836326a 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -93,13 +93,13 @@ class InterfaceCommonForm:
tagged_vlans = self.cleaned_data['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({
'mode': "An access interface cannot have tagged VLANs assigned."
})
# 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'] = []
@@ -250,7 +250,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class SiteCSVForm(forms.ModelForm):
status = CSVChoiceField(
- choices=SITE_STATUS_CHOICES,
+ choices=SiteStatusChoices,
required=False,
help_text='Operational status'
)
@@ -289,7 +289,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
widget=forms.MultipleHiddenInput
)
status = forms.ChoiceField(
- choices=add_blank_choice(SITE_STATUS_CHOICES),
+ choices=add_blank_choice(SiteStatusChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -338,7 +338,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
label='Search'
)
status = forms.MultipleChoiceField(
- choices=SITE_STATUS_CHOICES,
+ choices=SiteStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -500,7 +500,7 @@ class RackCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
- choices=RACK_STATUS_CHOICES,
+ choices=RackStatusChoices,
required=False,
help_text='Operational status'
)
@@ -514,19 +514,16 @@ class RackCSVForm(forms.ModelForm):
}
)
type = CSVChoiceField(
- choices=RACK_TYPE_CHOICES,
+ choices=RackTypeChoices,
required=False,
help_text='Rack type'
)
width = forms.ChoiceField(
- choices=(
- (RACK_WIDTH_19IN, '19'),
- (RACK_WIDTH_23IN, '23'),
- ),
+ choices=RackWidthChoices,
help_text='Rail-to-rail width (in inches)'
)
outer_unit = CSVChoiceField(
- choices=RACK_DIMENSION_UNIT_CHOICES,
+ choices=RackDimensionUnitChoices,
required=False,
help_text='Unit for outer dimensions'
)
@@ -598,7 +595,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(RACK_STATUS_CHOICES),
+ choices=add_blank_choice(RackStatusChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -620,12 +617,12 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
required=False
)
type = forms.ChoiceField(
- choices=add_blank_choice(RACK_TYPE_CHOICES),
+ choices=add_blank_choice(RackTypeChoices),
required=False,
widget=StaticSelect2()
)
width = forms.ChoiceField(
- choices=add_blank_choice(RACK_WIDTH_CHOICES),
+ choices=add_blank_choice(RackWidthChoices),
required=False,
widget=StaticSelect2()
)
@@ -647,7 +644,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
min_value=1
)
outer_unit = forms.ChoiceField(
- choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
+ choices=add_blank_choice(RackDimensionUnitChoices),
required=False,
widget=StaticSelect2()
)
@@ -692,7 +689,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
)
)
status = forms.MultipleChoiceField(
- choices=RACK_STATUS_CHOICES,
+ choices=RackStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -909,12 +906,10 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
value_field="slug",
)
)
- subdevice_role = forms.NullBooleanField(
+ subdevice_role = forms.MultipleChoiceField(
+ choices=add_blank_choice(SubdeviceRoleChoices),
required=False,
- label='Subdevice role',
- widget=StaticSelect2(
- choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
- )
+ widget=StaticSelect2Multiple()
)
console_ports = forms.NullBooleanField(
required=False,
@@ -981,7 +976,7 @@ class ConsolePortTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
widget=StaticSelect2()
)
@@ -1003,7 +998,7 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(ConsolePortTypes.CHOICES),
+ choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect2()
)
@@ -1025,7 +1020,7 @@ class PowerPortTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(PowerPortTypes.CHOICES),
+ choices=add_blank_choice(PowerPortTypeChoices),
required=False
)
maximum_draw = forms.IntegerField(
@@ -1067,7 +1062,7 @@ class PowerOutletTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(PowerOutletTypes.CHOICES),
+ choices=add_blank_choice(PowerOutletTypeChoices),
required=False
)
power_port = forms.ModelChoiceField(
@@ -1075,7 +1070,7 @@ class PowerOutletTemplateCreateForm(ComponentForm):
required=False
)
feed_leg = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_LEG_CHOICES),
+ choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False,
widget=StaticSelect2()
)
@@ -1108,7 +1103,7 @@ class InterfaceTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=IFACE_TYPE_CHOICES,
+ choices=InterfaceTypeChoices,
widget=StaticSelect2()
)
mgmt_only = forms.BooleanField(
@@ -1123,7 +1118,7 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
- choices=add_blank_choice(IFACE_TYPE_CHOICES),
+ choices=add_blank_choice(InterfaceTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -1165,7 +1160,7 @@ class FrontPortTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=PORT_TYPE_CHOICES,
+ choices=PortTypeChoices,
widget=StaticSelect2()
)
rear_port_set = forms.MultipleChoiceField(
@@ -1235,7 +1230,7 @@ class RearPortTemplateCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=PORT_TYPE_CHOICES,
+ choices=PortTypeChoices,
widget=StaticSelect2(),
)
positions = forms.IntegerField(
@@ -1334,7 +1329,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
- choices=InterfaceTypes.TYPE_CHOICES
+ choices=InterfaceTypeChoices.CHOICES
)
class Meta:
@@ -1343,15 +1338,10 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
'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):
type = forms.ChoiceField(
- choices=PortTypes.TYPE_CHOICES
+ choices=PortTypeChoices.CHOICES
)
rear_port = forms.ModelChoiceField(
queryset=RearPortTemplate.objects.all(),
@@ -1365,15 +1355,10 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
'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):
type = forms.ChoiceField(
- choices=PortTypes.TYPE_CHOICES
+ choices=PortTypeChoices.CHOICES
)
class Meta:
@@ -1382,11 +1367,6 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
'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):
@@ -1702,7 +1682,7 @@ class BaseDeviceCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
- choices=DEVICE_STATUS_CHOICES,
+ choices=DeviceStatusChoices,
help_text='Operational status'
)
@@ -1746,7 +1726,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
help_text='Name of parent rack'
)
face = CSVChoiceField(
- choices=RACK_FACE_CHOICES,
+ choices=DeviceFaceChoices,
required=False,
help_text='Mounted rack face'
)
@@ -1870,7 +1850,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(DEVICE_STATUS_CHOICES),
+ choices=add_blank_choice(DeviceStatusChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -1981,7 +1961,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
)
)
status = forms.MultipleChoiceField(
- choices=DEVICE_STATUS_CHOICES,
+ choices=DeviceStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -2063,7 +2043,7 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
type = forms.ChoiceField(
- choices=IFACE_TYPE_CHOICES,
+ choices=InterfaceTypeChoices,
widget=StaticSelect2()
)
enabled = forms.BooleanField(
@@ -2115,7 +2095,7 @@ class ConsolePortCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(ConsolePortTypes.CHOICES),
+ choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2172,7 +2152,7 @@ class ConsoleServerPortCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(ConsolePortTypes.CHOICES),
+ choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2191,7 +2171,7 @@ class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditF
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
- choices=add_blank_choice(ConsolePortTypes.CHOICES),
+ choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2264,7 +2244,7 @@ class PowerPortCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(PowerPortTypes.CHOICES),
+ choices=add_blank_choice(PowerPortTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2344,7 +2324,7 @@ class PowerOutletCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=add_blank_choice(PowerOutletTypes.CHOICES),
+ choices=add_blank_choice(PowerOutletTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2353,7 +2333,7 @@ class PowerOutletCreateForm(ComponentForm):
required=False
)
feed_leg = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_LEG_CHOICES),
+ choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False
)
description = forms.CharField(
@@ -2391,7 +2371,7 @@ class PowerOutletCSVForm(forms.ModelForm):
}
)
feed_leg = CSVChoiceField(
- choices=POWERFEED_LEG_CHOICES,
+ choices=PowerOutletFeedLegChoices,
required=False,
)
@@ -2428,11 +2408,11 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
- choices=PowerOutletTypes.CHOICES,
+ choices=PowerOutletTypeChoices,
required=False
)
feed_leg = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_LEG_CHOICES),
+ choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False,
)
power_port = forms.ModelChoiceField(
@@ -2529,12 +2509,14 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
if self.is_bound:
device = Device.objects.get(pk=self.data['device'])
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:
device = self.instance.device
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
@@ -2573,7 +2555,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
label='Name'
)
type = forms.ChoiceField(
- choices=IFACE_TYPE_CHOICES,
+ choices=InterfaceTypeChoices,
widget=StaticSelect2(),
)
enabled = forms.BooleanField(
@@ -2605,7 +2587,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
required=False
)
mode = forms.ChoiceField(
- choices=add_blank_choice(IFACE_MODE_CHOICES),
+ choices=add_blank_choice(InterfaceModeChoices),
required=False,
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)
if self.parent is not None:
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:
self.fields['lag'].queryset = Interface.objects.none()
@@ -2707,10 +2690,10 @@ class InterfaceCSVForm(forms.ModelForm):
}
)
type = CSVChoiceField(
- choices=IFACE_TYPE_CHOICES,
+ choices=InterfaceTypeChoices,
)
mode = CSVChoiceField(
- choices=IFACE_MODE_CHOICES,
+ choices=InterfaceModeChoices,
required=False,
)
@@ -2732,7 +2715,7 @@ class InterfaceCSVForm(forms.ModelForm):
if device:
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:
self.fields['lag'].queryset = Interface.objects.none()
@@ -2751,7 +2734,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
- choices=add_blank_choice(IFACE_TYPE_CHOICES),
+ choices=add_blank_choice(InterfaceTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2785,7 +2768,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
required=False
)
mode = forms.ChoiceField(
- choices=add_blank_choice(IFACE_MODE_CHOICES),
+ choices=add_blank_choice(InterfaceModeChoices),
required=False,
widget=StaticSelect2()
)
@@ -2821,7 +2804,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
if device is not None:
self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()],
- type=IFACE_TYPE_LAG
+ type=InterfaceTypeChoices.TYPE_LAG
)
else:
self.fields['lag'].choices = []
@@ -2911,7 +2894,7 @@ class FrontPortCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=PORT_TYPE_CHOICES,
+ choices=PortTypeChoices,
widget=StaticSelect2(),
)
rear_port_set = forms.MultipleChoiceField(
@@ -2983,7 +2966,7 @@ class FrontPortCSVForm(forms.ModelForm):
}
)
type = CSVChoiceField(
- choices=PORT_TYPE_CHOICES,
+ choices=PortTypeChoices,
)
class Meta:
@@ -3019,7 +3002,7 @@ class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
- choices=add_blank_choice(PORT_TYPE_CHOICES),
+ choices=add_blank_choice(PortTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -3077,7 +3060,7 @@ class RearPortCreateForm(ComponentForm):
label='Name'
)
type = forms.ChoiceField(
- choices=PORT_TYPE_CHOICES,
+ choices=PortTypeChoices,
widget=StaticSelect2(),
)
positions = forms.IntegerField(
@@ -3101,7 +3084,7 @@ class RearPortCSVForm(forms.ModelForm):
}
)
type = CSVChoiceField(
- choices=PORT_TYPE_CHOICES,
+ choices=PortTypeChoices,
)
class Meta:
@@ -3115,7 +3098,7 @@ class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
- choices=add_blank_choice(PORT_TYPE_CHOICES),
+ choices=add_blank_choice(PortTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -3449,12 +3432,12 @@ class CableCSVForm(forms.ModelForm):
help_text='Connection status'
)
type = CSVChoiceField(
- choices=CABLE_TYPE_CHOICES,
+ choices=CableTypeChoices,
required=False,
help_text='Cable type'
)
length_unit = CSVChoiceField(
- choices=CABLE_LENGTH_UNIT_CHOICES,
+ choices=CableLengthUnitChoices,
required=False,
help_text='Length unit'
)
@@ -3534,7 +3517,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
widget=forms.MultipleHiddenInput
)
type = forms.ChoiceField(
- choices=add_blank_choice(CABLE_TYPE_CHOICES),
+ choices=add_blank_choice(CableTypeChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -3559,7 +3542,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
required=False
)
length_unit = forms.ChoiceField(
- choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
+ choices=add_blank_choice(CableLengthUnitChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -3608,7 +3591,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
)
)
type = forms.MultipleChoiceField(
- choices=add_blank_choice(CABLE_TYPE_CHOICES),
+ choices=add_blank_choice(CableTypeChoices),
required=False,
widget=StaticSelect2()
)
@@ -3677,7 +3660,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
rack=device_bay.device.rack,
parent_bay__isnull=True,
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)
@@ -3725,7 +3708,7 @@ class DeviceBayCSVForm(forms.ModelForm):
rack=device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
- device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
+ device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
).exclude(pk=device.pk)
else:
self.fields['installed_device'].queryset = Interface.objects.none()
@@ -4214,22 +4197,22 @@ class PowerFeedCSVForm(forms.ModelForm):
help_text="Rack name (optional)"
)
status = CSVChoiceField(
- choices=POWERFEED_STATUS_CHOICES,
+ choices=PowerFeedStatusChoices,
required=False,
help_text='Operational status'
)
type = CSVChoiceField(
- choices=POWERFEED_TYPE_CHOICES,
+ choices=PowerFeedTypeChoices,
required=False,
help_text='Primary or redundant'
)
supply = CSVChoiceField(
- choices=POWERFEED_SUPPLY_CHOICES,
+ choices=PowerFeedSupplyChoices,
required=False,
help_text='AC/DC'
)
phase = CSVChoiceField(
- choices=POWERFEED_PHASE_CHOICES,
+ choices=PowerFeedPhaseChoices,
required=False,
help_text='Single or three-phase'
)
@@ -4289,25 +4272,25 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
+ choices=add_blank_choice(PowerFeedStatusChoices),
required=False,
initial='',
widget=StaticSelect2()
)
type = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
+ choices=add_blank_choice(PowerFeedTypeChoices),
required=False,
initial='',
widget=StaticSelect2()
)
supply = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
+ choices=add_blank_choice(PowerFeedSupplyChoices),
required=False,
initial='',
widget=StaticSelect2()
)
phase = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
+ choices=add_blank_choice(PowerFeedPhaseChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -4368,22 +4351,22 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
)
)
status = forms.MultipleChoiceField(
- choices=POWERFEED_STATUS_CHOICES,
+ choices=PowerFeedStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
type = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
+ choices=add_blank_choice(PowerFeedTypeChoices),
required=False,
widget=StaticSelect2()
)
supply = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
+ choices=add_blank_choice(PowerFeedSupplyChoices),
required=False,
widget=StaticSelect2()
)
phase = forms.ChoiceField(
- choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
+ choices=add_blank_choice(PowerFeedPhaseChoices),
required=False,
widget=StaticSelect2()
)
diff --git a/netbox/dcim/migrations/0078_3569_site_fields.py b/netbox/dcim/migrations/0078_3569_site_fields.py
new file mode 100644
index 000000000..8775abe5e
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0079_3569_rack_fields.py b/netbox/dcim/migrations/0079_3569_rack_fields.py
new file mode 100644
index 000000000..4e76a270f
--- /dev/null
+++ b/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),
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0080_3569_devicetype_fields.py b/netbox/dcim/migrations/0080_3569_devicetype_fields.py
new file mode 100644
index 000000000..e729eaa55
--- /dev/null
+++ b/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),
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0081_3569_device_fields.py b/netbox/dcim/migrations/0081_3569_device_fields.py
new file mode 100644
index 000000000..f1f0bdb2b
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0082_3569_interface_fields.py b/netbox/dcim/migrations/0082_3569_interface_fields.py
new file mode 100644
index 000000000..57701ce0a
--- /dev/null
+++ b/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),
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0082_3569_port_fields.py b/netbox/dcim/migrations/0082_3569_port_fields.py
new file mode 100644
index 000000000..6d8f50c32
--- /dev/null
+++ b/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
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0083_3569_cable_fields.py b/netbox/dcim/migrations/0083_3569_cable_fields.py
new file mode 100644
index 000000000..d6f013b37
--- /dev/null
+++ b/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),
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py
new file mode 100644
index 000000000..332443d0a
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py b/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py
new file mode 100644
index 000000000..e2c070584
--- /dev/null
+++ b/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),
+ ),
+
+ ]
diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
index 6fd008d0a..6f46c0c96 100644
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -245,9 +245,10 @@ class Site(ChangeLoggedModel, CustomFieldModel):
slug = models.SlugField(
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(
to='dcim.Region',
@@ -331,6 +332,12 @@ class Site(ChangeLoggedModel, CustomFieldModel):
'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:
ordering = ['name']
@@ -362,7 +369,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
)
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,
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(
to='dcim.RackRole',
@@ -497,15 +505,15 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
verbose_name='Asset tag',
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,
- null=True,
verbose_name='Type'
)
width = models.PositiveSmallIntegerField(
- choices=RACK_WIDTH_CHOICES,
- default=RACK_WIDTH_19IN,
+ choices=RackWidthChoices,
+ default=RackWidthChoices.WIDTH_19IN,
verbose_name='Width',
help_text='Rail-to-rail width'
)
@@ -527,10 +535,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
blank=True,
null=True
)
- outer_unit = models.PositiveSmallIntegerField(
- choices=RACK_DIMENSION_UNIT_CHOICES,
+ outer_unit = models.CharField(
+ max_length=50,
+ choices=RackDimensionUnitChoices,
blank=True,
- null=True
)
comments = models.TextField(
blank=True
@@ -552,6 +560,14 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
'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:
ordering = ['site', 'group', 'name']
unique_together = [
@@ -568,10 +584,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
def clean(self):
# 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")
elif self.outer_width is None and self.outer_depth is None:
- self.outer_unit = None
+ self.outer_unit = ''
if self.pk:
# Validate that Rack is tall enough to house the installed Devices
@@ -644,9 +660,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
return ""
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'}
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()]
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):
- 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()):
"""
@@ -910,12 +926,13 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
verbose_name='Is full depth',
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',
- 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(
blank=True
@@ -959,7 +976,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
self.part_number,
self.u_height,
self.is_full_depth,
- self.get_subdevice_role_display() if self.subdevice_role else None,
+ self.get_subdevice_role_display(),
self.comments,
)
@@ -979,13 +996,15 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
"{}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({
'subdevice_role': "Must delete all device bay templates associated with this device before "
"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({
'u_height': "Child device types must be 0U."
})
@@ -996,11 +1015,11 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
@property
def is_parent_device(self):
- return bool(self.subdevice_role)
+ return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT
@property
def is_child_device(self):
- return bool(self.subdevice_role is False)
+ return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
class ConsolePortTemplate(ComponentTemplateModel):
@@ -1017,7 +1036,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
)
type = models.CharField(
max_length=50,
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
blank=True
)
@@ -1052,7 +1071,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
)
type = models.CharField(
max_length=50,
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
blank=True
)
@@ -1087,7 +1106,7 @@ class PowerPortTemplate(ComponentTemplateModel):
)
type = models.CharField(
max_length=50,
- choices=PowerPortTypes.CHOICES,
+ choices=PowerPortTypeChoices,
blank=True
)
maximum_draw = models.PositiveSmallIntegerField(
@@ -1135,7 +1154,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
)
type = models.CharField(
max_length=50,
- choices=PowerOutletTypes.CHOICES,
+ choices=PowerOutletTypeChoices,
blank=True
)
power_port = models.ForeignKey(
@@ -1145,10 +1164,10 @@ class PowerOutletTemplate(ComponentTemplateModel):
null=True,
related_name='poweroutlet_templates'
)
- feed_leg = models.PositiveSmallIntegerField(
- choices=POWERFEED_LEG_CHOICES,
+ feed_leg = models.CharField(
+ max_length=50,
+ choices=PowerOutletFeedLegChoices,
blank=True,
- null=True,
help_text="Phase (for three-phase feeds)"
)
@@ -1194,9 +1213,9 @@ class InterfaceTemplate(ComponentTemplateModel):
name = models.CharField(
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(
default=False,
@@ -1233,8 +1252,9 @@ class FrontPortTemplate(ComponentTemplateModel):
name = models.CharField(
max_length=64
)
- type = models.PositiveSmallIntegerField(
- choices=PORT_TYPE_CHOICES
+ type = models.CharField(
+ max_length=50,
+ choices=PortTypeChoices
)
rear_port = models.ForeignKey(
to='dcim.RearPortTemplate',
@@ -1300,8 +1320,9 @@ class RearPortTemplate(ComponentTemplateModel):
name = models.CharField(
max_length=64
)
- type = models.PositiveSmallIntegerField(
- choices=PORT_TYPE_CHOICES
+ type = models.CharField(
+ max_length=50,
+ choices=PortTypeChoices
)
positions = models.PositiveSmallIntegerField(
default=1,
@@ -1526,16 +1547,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
verbose_name='Position (U)',
help_text='The lowest-numbered unit occupied by the device'
)
- face = models.PositiveSmallIntegerField(
+ face = models.CharField(
+ max_length=50,
blank=True,
- null=True,
- choices=RACK_FACE_CHOICES,
+ choices=DeviceFaceChoices,
verbose_name='Rack face'
)
- status = models.PositiveSmallIntegerField(
- choices=DEVICE_STATUS_CHOICES,
- default=DEVICE_STATUS_ACTIVE,
- verbose_name='Status'
+ status = models.CharField(
+ max_length=50,
+ choices=DeviceStatusChoices,
+ default=DeviceStatusChoices.STATUS_ACTIVE
)
primary_ip4 = models.OneToOneField(
to='ipam.IPAddress',
@@ -1597,6 +1618,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
]
+ STATUS_CLASS_MAP = {
+ DeviceStatusChoices.STATUS_OFFLINE: 'warning',
+ DeviceStatusChoices.STATUS_ACTIVE: 'success',
+ DeviceStatusChoices.STATUS_PLANNED: 'info',
+ DeviceStatusChoices.STATUS_STAGED: 'primary',
+ DeviceStatusChoices.STATUS_FAILED: 'danger',
+ DeviceStatusChoices.STATUS_INVENTORY: 'default',
+ DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning',
+ }
+
class Meta:
ordering = ['name']
unique_together = [
@@ -1625,7 +1656,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
})
if self.rack is None:
- if self.face is not None:
+ if self.face:
raise ValidationError({
'face': "Cannot select a rack face without assigning a rack.",
})
@@ -1635,7 +1666,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
})
# Validate position/face combination
- if self.position and self.face is None:
+ if self.position and not self.face:
raise ValidationError({
'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)
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(
max_length=50,
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
blank=True
)
connected_endpoint = models.OneToOneField(
@@ -1928,7 +1959,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
)
type = models.CharField(
max_length=50,
- choices=ConsolePortTypes.CHOICES,
+ choices=ConsolePortTypeChoices,
blank=True
)
connection_status = models.NullBooleanField(
@@ -1977,7 +2008,7 @@ class PowerPort(CableTermination, ComponentModel):
)
type = models.CharField(
max_length=50,
- choices=PowerPortTypes.CHOICES,
+ choices=PowerPortTypeChoices,
blank=True
)
maximum_draw = models.PositiveSmallIntegerField(
@@ -2077,8 +2108,8 @@ class PowerPort(CableTermination, ComponentModel):
}
# 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)
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
maximum_draw_total=Sum('maximum_draw'),
@@ -2120,7 +2151,7 @@ class PowerOutlet(CableTermination, ComponentModel):
)
type = models.CharField(
max_length=50,
- choices=PowerOutletTypes.CHOICES,
+ choices=PowerOutletTypeChoices,
blank=True
)
power_port = models.ForeignKey(
@@ -2130,10 +2161,10 @@ class PowerOutlet(CableTermination, ComponentModel):
null=True,
related_name='poweroutlets'
)
- feed_leg = models.PositiveSmallIntegerField(
- choices=POWERFEED_LEG_CHOICES,
+ feed_leg = models.CharField(
+ max_length=50,
+ choices=PowerOutletFeedLegChoices,
blank=True,
- null=True,
help_text="Phase (for three-phase feeds)"
)
connection_status = models.NullBooleanField(
@@ -2226,9 +2257,9 @@ class Interface(CableTermination, ComponentModel):
blank=True,
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(
default=True
@@ -2249,10 +2280,10 @@ class Interface(CableTermination, ComponentModel):
verbose_name='OOB 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,
- null=True
)
untagged_vlan = models.ForeignKey(
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.")
# 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({
'type': "Virtual machines can only have virtual interfaces."
})
@@ -2340,7 +2371,7 @@ class Interface(CableTermination, ComponentModel):
})
# 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({
'type': "Cannot change interface type; it has LAG members ({}).".format(
", ".join([iface.name for iface in self.member_interfaces.all()])
@@ -2361,7 +2392,7 @@ class Interface(CableTermination, ComponentModel):
self.untagged_vlan = None
# 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()
return super().save(*args, **kwargs)
@@ -2423,7 +2454,7 @@ class Interface(CableTermination, ComponentModel):
@property
def is_lag(self):
- return self.type == IFACE_TYPE_LAG
+ return self.type == InterfaceTypeChoices.TYPE_LAG
@property
def count_ipaddresses(self):
@@ -2446,8 +2477,9 @@ class FrontPort(CableTermination, ComponentModel):
name = models.CharField(
max_length=64
)
- type = models.PositiveSmallIntegerField(
- choices=PORT_TYPE_CHOICES
+ type = models.CharField(
+ max_length=50,
+ choices=PortTypeChoices
)
rear_port = models.ForeignKey(
to='dcim.RearPort',
@@ -2513,8 +2545,9 @@ class RearPort(CableTermination, ComponentModel):
name = models.CharField(
max_length=64
)
- type = models.PositiveSmallIntegerField(
- choices=PORT_TYPE_CHOICES
+ type = models.CharField(
+ max_length=50,
+ choices=PortTypeChoices
)
positions = models.PositiveSmallIntegerField(
default=1,
@@ -2776,10 +2809,10 @@ class Cable(ChangeLoggedModel):
ct_field='termination_b_type',
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(
choices=CONNECTION_STATUS_CHOICES,
@@ -2796,10 +2829,10 @@ class Cable(ChangeLoggedModel):
blank=True,
null=True
)
- length_unit = models.PositiveSmallIntegerField(
- choices=CABLE_LENGTH_UNIT_CHOICES,
+ length_unit = models.CharField(
+ max_length=50,
+ choices=CableLengthUnitChoices,
blank=True,
- null=True
)
# Stores the normalized length (in meters) for database ordering
_abs_length = models.DecimalField(
@@ -2927,10 +2960,10 @@ class Cable(ChangeLoggedModel):
))
# 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")
elif self.length is None:
- self.length_unit = None
+ self.length_unit = ''
def save(self, *args, **kwargs):
@@ -3074,21 +3107,25 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
name = models.CharField(
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(
validators=[MinValueValidator(1)],
@@ -3123,6 +3160,18 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
'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:
ordering = ['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
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)
else:
self.available_power = round(kva)
@@ -3170,7 +3219,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
super().save(*args, **kwargs)
def get_type_class(self):
- return STATUS_CLASSES[self.type]
+ return self.TYPE_CLASS_MAP.get(self.type)
def get_status_class(self):
- return STATUS_CLASSES[self.status]
+ return self.STATUS_CLASS_MAP.get(self.status)
diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
index 1b5a32700..c662da8ec 100644
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -156,10 +156,6 @@ DEVICE_PRIMARY_IP = """
{{ 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 = """
{{ record.instance_count }}
"""
@@ -391,10 +387,6 @@ class DeviceTypeTable(BaseTable):
verbose_name='Device Type'
)
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(
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances'
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index 9c873c886..c396407ac 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -3,6 +3,7 @@ from netaddr import IPNetwork
from rest_framework import status
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
+from dcim.choices import *
from dcim.constants import *
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -180,7 +181,7 @@ class SiteTest(APITestCase):
'name': 'Test Site 4',
'slug': 'test-site-4',
'region': self.region1.pk,
- 'status': SITE_STATUS_ACTIVE,
+ 'status': SiteStatusChoices.STATUS_ACTIVE,
}
url = reverse('dcim-api:site-list')
@@ -200,19 +201,19 @@ class SiteTest(APITestCase):
'name': 'Test Site 4',
'slug': 'test-site-4',
'region': self.region1.pk,
- 'status': SITE_STATUS_ACTIVE,
+ 'status': SiteStatusChoices.STATUS_ACTIVE,
},
{
'name': 'Test Site 5',
'slug': 'test-site-5',
'region': self.region1.pk,
- 'status': SITE_STATUS_ACTIVE,
+ 'status': SiteStatusChoices.STATUS_ACTIVE,
},
{
'name': 'Test Site 6',
'slug': 'test-site-6',
'region': self.region1.pk,
- 'status': SITE_STATUS_ACTIVE,
+ 'status': SiteStatusChoices.STATUS_ACTIVE,
},
]
@@ -2473,7 +2474,7 @@ class InterfaceTest(APITestCase):
data = {
'device': self.device.pk,
'name': 'Test Interface 4',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan3.id,
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
}
@@ -2520,21 +2521,21 @@ class InterfaceTest(APITestCase):
{
'device': self.device.pk,
'name': 'Test Interface 4',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
{
'device': self.device.pk,
'name': 'Test Interface 5',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
{
'device': self.device.pk,
'name': 'Test Interface 6',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
@@ -2553,7 +2554,7 @@ class InterfaceTest(APITestCase):
def test_update_interface(self):
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 = {
@@ -2590,11 +2591,11 @@ class DeviceBayTest(APITestCase):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype1 = DeviceType.objects.create(
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(
manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
- subdevice_role=SUBDEVICE_ROLE_CHILD
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
)
devicerole = DeviceRole.objects.create(
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 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(
termination_a=self.device1.interfaces.get(name='eth0'),
@@ -3033,16 +3034,16 @@ class ConnectionTest(APITestCase):
device=self.device2, name='Test Console Server Port 1'
)
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(
- 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(
- 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(
- 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')
@@ -3161,16 +3162,16 @@ class ConnectionTest(APITestCase):
device=self.device2, name='Test Interface 2'
)
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(
- 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(
- 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(
- 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')
@@ -3272,16 +3273,16 @@ class ConnectionTest(APITestCase):
circuit=circuit, term_side='A', site=self.site, port_speed=10000
)
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(
- 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(
- 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(
- 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')
@@ -3410,23 +3411,23 @@ class VirtualChassisTest(APITestCase):
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
)
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):
- 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):
- 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):
- 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):
- 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):
- 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):
- 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):
- 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):
- 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
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'
)
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(
- 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(
- 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(
- 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(
- 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(
- 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):
@@ -3726,7 +3727,7 @@ class PowerFeedTest(APITestCase):
'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
- 'type': POWERFEED_TYPE_PRIMARY,
+ 'type': PowerFeedTypeChoices.TYPE_PRIMARY,
}
url = reverse('dcim-api:powerfeed-list')
@@ -3746,13 +3747,13 @@ class PowerFeedTest(APITestCase):
'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
- 'type': POWERFEED_TYPE_PRIMARY,
+ 'type': PowerFeedTypeChoices.TYPE_PRIMARY,
},
{
'name': 'Test Power Feed 4B',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
- 'type': POWERFEED_TYPE_REDUNDANT,
+ 'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
},
]
@@ -3769,7 +3770,7 @@ class PowerFeedTest(APITestCase):
data = {
'name': 'Test Power Feed X',
'rack': self.rack4.pk,
- 'type': POWERFEED_TYPE_REDUNDANT,
+ 'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
}
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py
index 2f333ea69..d7a946568 100644
--- a/netbox/dcim/tests/test_forms.py
+++ b/netbox/dcim/tests/test_forms.py
@@ -21,10 +21,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
- 'face': RACK_FACE_FRONT,
+ 'face': DeviceFaceChoices.FACE_FRONT,
'position': 41,
'platform': get_id(Platform, 'juniper-junos'),
- 'status': DEVICE_STATUS_ACTIVE,
+ 'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save())
@@ -38,10 +38,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
- 'face': RACK_FACE_FRONT,
+ 'face': DeviceFaceChoices.FACE_FRONT,
'position': 1,
'platform': get_id(Platform, 'juniper-junos'),
- 'status': DEVICE_STATUS_ACTIVE,
+ 'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertFalse(test.is_valid())
@@ -54,10 +54,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
- 'face': None,
+ 'face': '',
'position': None,
'platform': None,
- 'status': DEVICE_STATUS_ACTIVE,
+ 'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
@@ -71,10 +71,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
- 'face': RACK_FACE_REAR,
+ 'face': DeviceFaceChoices.FACE_REAR,
'position': None,
'platform': None,
- 'status': DEVICE_STATUS_ACTIVE,
+ 'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index 2b5bed283..2c3507758 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -87,7 +87,7 @@ class RackTestCase(TestCase):
site=self.site1,
rack=rack1,
position=43,
- face=RACK_FACE_FRONT,
+ face=DeviceFaceChoices.FACE_FRONT,
)
device1.save()
@@ -117,7 +117,7 @@ class RackTestCase(TestCase):
site=self.site1,
rack=self.rack,
position=10,
- face=RACK_FACE_REAR,
+ face=DeviceFaceChoices.FACE_REAR,
)
device1.save()
@@ -146,7 +146,7 @@ class RackTestCase(TestCase):
site=self.site1,
rack=self.rack,
position=None,
- face=None,
+ face='',
)
self.assertTrue(pdu)
@@ -187,20 +187,20 @@ class DeviceTestCase(TestCase):
device_type=self.device_type,
name='Power Outlet 1',
power_port=ppt,
- feed_leg=POWERFEED_LEG_A
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
).save()
InterfaceTemplate(
device_type=self.device_type,
name='Interface 1',
- type=IFACE_TYPE_1GE_FIXED,
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
mgmt_only=True
).save()
rpt = RearPortTemplate(
device_type=self.device_type,
name='Rear Port 1',
- type=PORT_TYPE_8P8C,
+ type=PortTypeChoices.TYPE_8P8C,
positions=8
)
rpt.save()
@@ -208,7 +208,7 @@ class DeviceTestCase(TestCase):
FrontPortTemplate(
device_type=self.device_type,
name='Front Port 1',
- type=PORT_TYPE_8P8C,
+ type=PortTypeChoices.TYPE_8P8C,
rear_port=rpt,
rear_port_position=2
).save()
@@ -251,27 +251,27 @@ class DeviceTestCase(TestCase):
device=d,
name='Power Outlet 1',
power_port=pp,
- feed_leg=POWERFEED_LEG_A
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
)
Interface.objects.get(
device=d,
name='Interface 1',
- type=IFACE_TYPE_1GE_FIXED,
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
mgmt_only=True
)
rp = RearPort.objects.get(
device=d,
name='Rear Port 1',
- type=PORT_TYPE_8P8C,
+ type=PortTypeChoices.TYPE_8P8C,
positions=8
)
FrontPort.objects.get(
device=d,
name='Front Port 1',
- type=PORT_TYPE_8P8C,
+ type=PortTypeChoices.TYPE_8P8C,
rear_port=rp,
rear_port_position=2
)
@@ -379,7 +379,7 @@ class CableTestCase(TestCase):
"""
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)
with self.assertRaises(ValidationError):
cable.clean()
@@ -388,7 +388,7 @@ class CableTestCase(TestCase):
"""
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)
with self.assertRaises(ValidationError):
cable.clean()
@@ -421,16 +421,16 @@ class CablePathTestCase(TestCase):
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
)
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(
- 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(
- 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(
- 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):
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 74457af0e..8eaab77d4 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -255,15 +255,15 @@ power-outlets:
- name: Power Outlet 1
type: iec-60320-c13
power_port: Power Port 1
- feed_leg: 1
+ feed_leg: A
- name: Power Outlet 2
type: iec-60320-c13
power_port: Power Port 1
- feed_leg: 1
+ feed_leg: A
- name: Power Outlet 3
type: iec-60320-c13
power_port: Power Port 1
- feed_leg: 1
+ feed_leg: A
interfaces:
- name: Interface 1
type: 1000base-t
@@ -326,29 +326,29 @@ device-bays:
self.assertEqual(dt.consoleport_templates.count(), 3)
cp1 = ConsolePortTemplate.objects.first()
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)
csp1 = ConsoleServerPortTemplate.objects.first()
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)
pp1 = PowerPortTemplate.objects.first()
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)
po1 = PowerOutletTemplate.objects.first()
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.feed_leg, POWERFEED_LEG_A)
+ self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(dt.interface_templates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
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.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.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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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):
url = reverse('dcim:cable_list')
params = {
- "type": CABLE_TYPE_CAT6,
+ "type": CableTypeChoices.TYPE_CAT6,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py
index 42dc486b8..0def32fde 100644
--- a/netbox/extras/api/customfields.py
+++ b/netbox/extras/api/customfields.py
@@ -5,7 +5,7 @@ from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
-from extras.constants import *
+from extras.choices import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from utilities.api import ValidatedModelSerializer
@@ -37,7 +37,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
if value not in [None, '']:
# Validate integer
- if cf.type == CF_TYPE_INTEGER:
+ if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
try:
int(value)
except ValueError:
@@ -46,13 +46,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
)
# 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(
"Invalid value for boolean field {}: {}".format(field_name, value)
)
# Validate date
- if cf.type == CF_TYPE_DATE:
+ if cf.type == CustomFieldTypeChoices.TYPE_DATE:
try:
datetime.strptime(value, '%Y-%m-%d')
except ValueError:
@@ -61,7 +61,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
)
# Validate selected choice
- if cf.type == CF_TYPE_SELECT:
+ if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
try:
value = int(value)
except ValueError:
@@ -100,7 +100,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
instance.custom_fields = {}
for field in fields:
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
else:
instance.custom_fields[field.name] = value
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index 7c533a5b4..f648c2187 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -8,6 +8,7 @@ from dcim.api.nested_serializers import (
NestedRegionSerializer, NestedSiteSerializer,
)
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
+from extras.choices import *
from extras.constants import *
from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
@@ -56,8 +57,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer):
template_language = ChoiceField(
- choices=TEMPLATE_LANGUAGE_CHOICES,
- default=TEMPLATE_LANGUAGE_JINJA2
+ choices=ExportTemplateLanguageChoices,
+ default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
)
class Meta:
@@ -255,7 +256,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
read_only=True
)
action = ChoiceField(
- choices=OBJECTCHANGE_ACTION_CHOICES,
+ choices=ObjectChangeActionChoices,
read_only=True
)
changed_object_type = ContentTypeField(
diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py
new file mode 100644
index 000000000..b483d098f
--- /dev/null
+++ b/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,
+ }
diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py
index 2b4077372..c64715203 100644
--- a/netbox/extras/constants.py
+++ b/netbox/extras/constants.py
@@ -19,32 +19,6 @@ CUSTOMFIELD_MODELS = [
'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
CUSTOMLINK_MODELS = [
'circuits.circuit',
@@ -68,23 +42,6 @@ CUSTOMLINK_MODELS = [
'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_TYPE_INTERFACE = 100
GRAPH_TYPE_DEVICE = 150
@@ -128,42 +85,6 @@ EXPORTTEMPLATE_MODELS = [
'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
LOG_DEFAULT = 0
LOG_SUCCESS = 10
@@ -178,14 +99,6 @@ LOG_LEVEL_CODES = {
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
WEBHOOK_MODELS = [
'circuits.circuit',
diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py
index 7a9862760..b9bc013d1 100644
--- a/netbox/extras/filters.py
+++ b/netbox/extras/filters.py
@@ -4,6 +4,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
+from .choices import *
from .constants import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
@@ -25,7 +26,7 @@ class CustomFieldFilter(django_filters.Filter):
return queryset
# Selection fields get special treatment (values must be integers)
- if self.cf_type == CF_TYPE_SELECT:
+ if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
try:
# Treat 0 as None
if int(value) == 0:
@@ -42,7 +43,8 @@ class CustomFieldFilter(django_filters.Filter):
return queryset.none()
# 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(
custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value=value
@@ -65,7 +67,11 @@ class CustomFieldFilterSet(django_filters.FilterSet):
super().__init__(*args, **kwargs)
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:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py
index efb92b2ce..b9f2d1538 100644
--- a/netbox/extras/forms.py
+++ b/netbox/extras/forms.py
@@ -13,6 +13,7 @@ from utilities.forms import (
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES,
)
+from .choices import *
from .constants import *
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()
custom_fields = CustomField.objects.filter(obj_type=content_type)
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:
field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer
- if cf.type == CF_TYPE_INTEGER:
+ if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean
- elif cf.type == CF_TYPE_BOOLEAN:
+ elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
choices = (
(None, '---------'),
(1, 'True'),
@@ -56,11 +57,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
)
# 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")
# Select
- elif cf.type == CF_TYPE_SELECT:
+ elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only:
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)
# URL
- elif cf.type == CF_TYPE_URL:
+ elif cf.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial)
# Text
@@ -400,7 +401,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
)
)
action = forms.ChoiceField(
- choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
+ choices=add_blank_choice(ObjectChangeActionChoices),
required=False
)
user = forms.ModelChoiceField(
diff --git a/netbox/extras/middleware.py b/netbox/extras/middleware.py
index 57f8f37d1..f305c1d56 100644
--- a/netbox/extras/middleware.py
+++ b/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 utilities.querysets import DummyQuerySet
-from .constants import *
+from .choices import ObjectChangeActionChoices
from .models import ObjectChange
from .signals import purge_changelog
from .webhooks import enqueue_webhooks
@@ -23,7 +23,7 @@ def handle_changed_object(sender, instance, **kwargs):
Fires when an object is created or updated.
"""
# 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(
(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
_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:
# 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'):
instance.cache_custom_fields()
@@ -116,11 +116,11 @@ class ObjectChangeMiddleware(object):
enqueue_webhooks(instance, request.user, request.id, action)
# Increment metric counters
- if action == OBJECTCHANGE_ACTION_CREATE:
+ if action == ObjectChangeActionChoices.ACTION_CREATE:
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()
- elif action == OBJECTCHANGE_ACTION_DELETE:
+ elif action == ObjectChangeActionChoices.ACTION_DELETE:
model_deletes.labels(instance._meta.model_name).inc()
# Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in
diff --git a/netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py b/netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py
index c6167ff9f..098f234dc 100644
--- a/netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py
+++ b/netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py
@@ -8,8 +8,6 @@ import django.db.models.deletion
import extras.models
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):
"""
diff --git a/netbox/extras/migrations/0010_customfield_filter_logic.py b/netbox/extras/migrations/0010_customfield_filter_logic.py
index dbff03e2d..dcc2d6ad6 100644
--- a/netbox/extras/migrations/0010_customfield_filter_logic.py
+++ b/netbox/extras/migrations/0010_customfield_filter_logic.py
@@ -2,21 +2,19 @@
# Generated by Django 1.11.9 on 2018-02-21 19:48
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):
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
- 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):
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):
diff --git a/netbox/extras/migrations/0029_3569_customfield_fields.py b/netbox/extras/migrations/0029_3569_customfield_fields.py
new file mode 100644
index 000000000..23174a4c7
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/extras/migrations/0030_3569_objectchange_fields.py b/netbox/extras/migrations/0030_3569_objectchange_fields.py
new file mode 100644
index 000000000..c31f925e2
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/extras/migrations/0031_3569_exporttemplate_fields.py b/netbox/extras/migrations/0031_3569_exporttemplate_fields.py
new file mode 100644
index 000000000..2860f732e
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/extras/migrations/0032_3569_webhook_fields.py b/netbox/extras/migrations/0032_3569_webhook_fields.py
new file mode 100644
index 000000000..a7bd2e3b5
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/extras/models.py b/netbox/extras/models.py
index 8f652f4eb..48d61ee0f 100644
--- a/netbox/extras/models.py
+++ b/netbox/extras/models.py
@@ -15,6 +15,7 @@ from taggit.models import TagBase, GenericTaggedItemBase
from utilities.fields import ColorField
from utilities.utils import deepmerge, model_names_to_filter_dict
+from .choices import *
from .constants import *
from .querysets import ConfigContextQuerySet
@@ -62,9 +63,10 @@ class Webhook(models.Model):
verbose_name='URL',
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'
)
additional_headers = JSONField(
@@ -182,9 +184,10 @@ class CustomField(models.Model):
limit_choices_to=get_custom_field_models,
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(
max_length=50,
@@ -205,9 +208,10 @@ class CustomField(models.Model):
help_text='If true, this field is required when creating new objects '
'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 '
'matches the entire field.'
)
@@ -233,15 +237,15 @@ class CustomField(models.Model):
"""
if value is None:
return ''
- if self.type == CF_TYPE_BOOLEAN:
+ if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
return str(int(bool(value)))
- if self.type == CF_TYPE_DATE:
+ if self.type == CustomFieldTypeChoices.TYPE_DATE:
# Could be date/datetime object or string
try:
return value.strftime('%Y-%m-%d')
except AttributeError:
return value
- if self.type == CF_TYPE_SELECT:
+ if self.type == CustomFieldTypeChoices.TYPE_SELECT:
# Could be ModelChoiceField or TypedChoiceField
return str(value.id) if hasattr(value, 'id') else str(value)
return value
@@ -252,14 +256,14 @@ class CustomField(models.Model):
"""
if serialized_value == '':
return None
- if self.type == CF_TYPE_INTEGER:
+ if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
return int(serialized_value)
- if self.type == CF_TYPE_BOOLEAN:
+ if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
return bool(int(serialized_value))
- if self.type == CF_TYPE_DATE:
+ if self.type == CustomFieldTypeChoices.TYPE_DATE:
# Read date as YYYY-MM-DD
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 serialized_value
@@ -312,7 +316,7 @@ class CustomFieldChoice(models.Model):
to='extras.CustomField',
on_delete=models.CASCADE,
related_name='choices',
- limit_choices_to={'type': CF_TYPE_SELECT}
+ limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
)
value = models.CharField(
max_length=100
@@ -330,14 +334,17 @@ class CustomFieldChoice(models.Model):
return self.value
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.")
def delete(self, using=None, keep_parents=False):
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
pk = self.pk
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(
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"
)
new_window = models.BooleanField(
@@ -458,9 +465,10 @@ class ExportTemplate(models.Model):
max_length=200,
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(
help_text='The list of objects being exported is passed as a context variable named queryset
.'
@@ -796,8 +804,9 @@ class ObjectChange(models.Model):
request_id = models.UUIDField(
editable=False
)
- action = models.PositiveSmallIntegerField(
- choices=OBJECTCHANGE_ACTION_CHOICES
+ action = models.CharField(
+ max_length=50,
+ choices=ObjectChangeActionChoices
)
changed_object_type = models.ForeignKey(
to=ContentType,
diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py
index a5545693e..dca84dfdc 100644
--- a/netbox/extras/tables.py
+++ b/netbox/extras/tables.py
@@ -38,11 +38,11 @@ OBJECTCHANGE_TIME = """
"""
OBJECTCHANGE_ACTION = """
-{% if record.action == 1 %}
+{% if record.action == 'create' %}
Created
-{% elif record.action == 2 %}
+{% elif record.action == 'update' %}
Updated
-{% elif record.action == 3 %}
+{% elif record.action == 'delete' %}
Deleted
{% endif %}
"""
diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py
index 22b4912b9..8f01cc3bf 100644
--- a/netbox/extras/tests/test_changelog.py
+++ b/netbox/extras/tests/test_changelog.py
@@ -3,6 +3,7 @@ from django.urls import reverse
from rest_framework import status
from dcim.models import Site
+from extras.choices import *
from extras.constants import *
from extras.models import CustomField, CustomFieldValue, ObjectChange
from utilities.testing import APITestCase
@@ -17,7 +18,7 @@ class ChangeLogTest(APITestCase):
# Create a custom field on the Site model
ct = ContentType.objects.get_for_model(Site)
cf = CustomField(
- type=CF_TYPE_TEXT,
+ type=CustomFieldTypeChoices.TYPE_TEXT,
name='my_field',
required=False
)
@@ -49,7 +50,7 @@ class ChangeLogTest(APITestCase):
changed_object_id=site.pk
)
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.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
@@ -81,7 +82,7 @@ class ChangeLogTest(APITestCase):
changed_object_id=site.pk
)
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.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
@@ -110,6 +111,6 @@ class ChangeLogTest(APITestCase):
oc = ObjectChange.objects.first()
self.assertEqual(oc.changed_object, None)
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.assertListEqual(sorted(oc.object_data['tags']), ['bar', 'foo'])
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 96f3483bc..362b96931 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -6,7 +6,7 @@ from django.urls import reverse
from rest_framework import status
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 utilities.testing import APITestCase
from virtualization.models import VirtualMachine
@@ -25,13 +25,13 @@ class CustomFieldTest(TestCase):
def test_simple_fields(self):
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)
@@ -67,7 +67,7 @@ class CustomFieldTest(TestCase):
obj_type = ContentType.objects.get_for_model(Site)
# 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.obj_type.set([obj_type])
cf.save()
@@ -107,37 +107,37 @@ class CustomFieldAPITest(APITestCase):
content_type = ContentType.objects.get_for_model(Site)
# 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.obj_type.set([content_type])
self.cf_text.save()
# 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.obj_type.set([content_type])
self.cf_integer.save()
# 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.obj_type.set([content_type])
self.cf_boolean.save()
# 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.obj_type.set([content_type])
self.cf_date.save()
# 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.obj_type.set([content_type])
self.cf_url.save()
# 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.obj_type.set([content_type])
self.cf_select.save()
@@ -308,8 +308,8 @@ class CustomFieldChoiceAPITest(APITestCase):
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_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50)
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index 0f3625191..792390121 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -6,7 +6,7 @@ from django.test import Client, TestCase
from django.urls import reverse
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 utilities.testing import create_test_user
@@ -83,7 +83,7 @@ class ObjectChangeTestCase(TestCase):
# Create three ObjectChanges
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.request_id = uuid.uuid4()
oc.save()
diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py
index ac82bbb31..c95cb9f31 100644
--- a/netbox/extras/webhooks.py
+++ b/netbox/extras/webhooks.py
@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from extras.models import Webhook
from utilities.api import get_serializer_for_model
+from .choices import *
from .constants import *
@@ -18,9 +19,9 @@ def enqueue_webhooks(instance, user, request_id, action):
# Retrieve any applicable Webhooks
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]
obj_type = ContentType.objects.get_for_model(instance.__class__)
webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py
index 9a637e852..e8c13f2a0 100644
--- a/netbox/extras/webhooks_worker.py
+++ b/netbox/extras/webhooks_worker.py
@@ -6,6 +6,7 @@ import requests
from django_rq import job
from rest_framework.utils.encoders import JSONEncoder
+from .choices import ObjectChangeActionChoices
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
"""
payload = {
- 'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
+ 'event': dict(ObjectChangeActionChoices)[event].lower(),
'timestamp': timestamp,
'model': model_name,
'username': username,
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
index fc0c390cf..5ebc52390 100644
--- a/netbox/ipam/api/serializers.py
+++ b/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.models import Interface
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 utilities.api import (
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
@@ -102,7 +102,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
group = NestedVLANGroupSerializer(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)
tags = TagListSerializerField(required=False)
prefix_count = serializers.IntegerField(read_only=True)
@@ -140,7 +140,7 @@ class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(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)
tags = TagListSerializerField(required=False)
@@ -200,8 +200,8 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
family = ChoiceField(choices=AF_CHOICES, read_only=True)
vrf = NestedVRFSerializer(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)
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
nat_outside = NestedIPAddressSerializer(read_only=True)
@@ -239,7 +239,7 @@ class AvailableIPSerializer(serializers.Serializer):
class ServiceSerializer(CustomFieldModelSerializer):
device = NestedDeviceSerializer(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(
queryset=IPAddress.objects.all(),
serializer=NestedIPAddressSerializer,
diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py
new file mode 100644
index 000000000..543608b33
--- /dev/null
+++ b/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,
+ }
diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py
deleted file mode 100644
index e2fd1ca01..000000000
--- a/netbox/ipam/constants.py
+++ /dev/null
@@ -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'),
-)
diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py
index c54ba2f62..3445db80d 100644
--- a/netbox/ipam/filters.py
+++ b/netbox/ipam/filters.py
@@ -9,7 +9,7 @@ from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine
-from .constants import *
+from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@@ -178,7 +178,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
label='Role (slug)',
)
status = django_filters.MultipleChoiceFilter(
- choices=PREFIX_STATUS_CHOICES,
+ choices=PrefixStatusChoices,
null_value=None
)
tag = TagFilter()
@@ -310,11 +310,11 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
label='Interface (ID)',
)
status = django_filters.MultipleChoiceFilter(
- choices=IPADDRESS_STATUS_CHOICES,
+ choices=IPAddressStatusChoices,
null_value=None
)
role = django_filters.MultipleChoiceFilter(
- choices=IPADDRESS_ROLE_CHOICES
+ choices=IPAddressRoleChoices
)
tag = TagFilter()
@@ -424,7 +424,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Role (slug)',
)
status = django_filters.MultipleChoiceFilter(
- choices=VLAN_STATUS_CHOICES,
+ choices=VLANStatusChoices,
null_value=None
)
tag = TagFilter()
diff --git a/netbox/ipam/fixtures/ipam.json b/netbox/ipam/fixtures/ipam.json
index 10e22b2d7..e722b3629 100644
--- a/netbox/ipam/fixtures/ipam.json
+++ b/netbox/ipam/fixtures/ipam.json
@@ -40,7 +40,7 @@
"site": 1,
"vrf": null,
"vlan": null,
- "status": 1,
+ "status": "active",
"role": 1,
"description": ""
}
@@ -56,7 +56,7 @@
"site": 1,
"vrf": null,
"vlan": null,
- "status": 1,
+ "status": "active",
"role": 1,
"description": ""
}
@@ -322,7 +322,7 @@
"site": 1,
"vid": 999,
"name": "TEST",
- "status": 1,
+ "status": "active",
"role": 1
}
}
diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py
index bedbe3463..613fc2865 100644
--- a/netbox/ipam/forms.py
+++ b/netbox/ipam/forms.py
@@ -13,7 +13,7 @@ from utilities.forms import (
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
)
from virtualization.models import VirtualMachine
-from .constants import *
+from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
IP_FAMILY_CHOICES = [
@@ -374,7 +374,7 @@ class PrefixCSVForm(forms.ModelForm):
required=False
)
status = CSVChoiceField(
- choices=PREFIX_STATUS_CHOICES,
+ choices=PrefixStatusChoices,
help_text='Operational status'
)
role = forms.ModelChoiceField(
@@ -459,7 +459,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(PREFIX_STATUS_CHOICES),
+ choices=add_blank_choice(PrefixStatusChoices),
required=False,
widget=StaticSelect2()
)
@@ -527,7 +527,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
)
)
status = forms.MultipleChoiceField(
- choices=PREFIX_STATUS_CHOICES,
+ choices=PrefixStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -764,11 +764,11 @@ class IPAddressCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
- choices=IPADDRESS_STATUS_CHOICES,
+ choices=IPAddressStatusChoices,
help_text='Operational status'
)
role = CSVChoiceField(
- choices=IPADDRESS_ROLE_CHOICES,
+ choices=IPAddressRoleChoices,
required=False,
help_text='Functional role'
)
@@ -893,12 +893,12 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(IPADDRESS_STATUS_CHOICES),
+ choices=add_blank_choice(IPAddressStatusChoices),
required=False,
widget=StaticSelect2()
)
role = forms.ChoiceField(
- choices=add_blank_choice(IPADDRESS_ROLE_CHOICES),
+ choices=add_blank_choice(IPAddressRoleChoices),
required=False,
widget=StaticSelect2()
)
@@ -972,12 +972,12 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
)
)
status = forms.MultipleChoiceField(
- choices=IPADDRESS_STATUS_CHOICES,
+ choices=IPAddressStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
role = forms.MultipleChoiceField(
- choices=IPADDRESS_ROLE_CHOICES,
+ choices=IPAddressRoleChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -1111,7 +1111,7 @@ class VLANCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
- choices=VLAN_STATUS_CHOICES,
+ choices=VLANStatusChoices,
help_text='Operational status'
)
role = forms.ModelChoiceField(
@@ -1180,7 +1180,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
)
)
status = forms.ChoiceField(
- choices=add_blank_choice(VLAN_STATUS_CHOICES),
+ choices=add_blank_choice(VLANStatusChoices),
required=False,
widget=StaticSelect2()
)
@@ -1229,7 +1229,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
)
)
status = forms.MultipleChoiceField(
- choices=VLAN_STATUS_CHOICES,
+ choices=VLANStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -1292,7 +1292,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Search'
)
protocol = forms.ChoiceField(
- choices=add_blank_choice(IP_PROTOCOL_CHOICES),
+ choices=add_blank_choice(ServiceProtocolChoices),
required=False,
widget=StaticSelect2Multiple()
)
@@ -1307,7 +1307,7 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
widget=forms.MultipleHiddenInput()
)
protocol = forms.ChoiceField(
- choices=add_blank_choice(IP_PROTOCOL_CHOICES),
+ choices=add_blank_choice(ServiceProtocolChoices),
required=False,
widget=StaticSelect2()
)
diff --git a/netbox/ipam/migrations/0028_3569_prefix_fields.py b/netbox/ipam/migrations/0028_3569_prefix_fields.py
new file mode 100644
index 000000000..f0db5f403
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py
new file mode 100644
index 000000000..e5556900e
--- /dev/null
+++ b/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),
+ ),
+
+ ]
diff --git a/netbox/ipam/migrations/0030_3569_vlan_fields.py b/netbox/ipam/migrations/0030_3569_vlan_fields.py
new file mode 100644
index 000000000..ac3bc47ec
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/ipam/migrations/0031_3569_service_fields.py b/netbox/ipam/migrations/0031_3569_service_fields.py
new file mode 100644
index 000000000..f06d9ff84
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index 8f9b64b59..79a6f48ad 100644
--- a/netbox/ipam/models.py
+++ b/netbox/ipam/models.py
@@ -14,12 +14,29 @@ from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from virtualization.models import VirtualMachine
-from .constants import *
+from .choices import *
from .fields import IPNetworkField, IPAddressField
from .querysets import PrefixQuerySet
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):
"""
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,
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',
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',
]
+ STATUS_CLASS_MAP = {
+ 'container': 'default',
+ 'active': 'primary',
+ 'reserved': 'info',
+ 'deprecated': 'danger',
+ }
+
class Meta:
ordering = [F('vrf').asc(nulls_first=True), 'family', 'prefix']
verbose_name_plural = 'prefixes'
@@ -404,7 +429,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
prefix_length = property(fset=_set_prefix_length)
def get_status_class(self):
- return STATUS_CHOICE_CLASSES[self.status]
+ return self.STATUS_CLASS_MAP.get(self.status)
def get_duplicates(self):
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
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))
else:
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
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))
else:
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
"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)
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
@@ -550,17 +575,16 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
blank=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'
)
- role = models.PositiveSmallIntegerField(
- verbose_name='Role',
- choices=IPADDRESS_ROLE_CHOICES,
+ role = models.CharField(
+ max_length=50,
+ choices=IPAddressRoleChoices,
blank=True,
- null=True,
help_text='The functional role of this IP'
)
interface = models.ForeignKey(
@@ -604,6 +628,24 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
'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:
ordering = ['family', 'address']
verbose_name = 'IP address'
@@ -737,10 +779,10 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
return None
def get_status_class(self):
- return STATUS_CHOICE_CLASSES[self.status]
+ return self.STATUS_CLASS_MAP.get(self.status)
def get_role_class(self):
- return ROLE_CHOICE_CLASSES[self.role]
+ return self.ROLE_CLASS_MAP[self.role]
class VLANGroup(ChangeLoggedModel):
@@ -831,10 +873,10 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
blank=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(
to='ipam.Role',
@@ -857,6 +899,12 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
+ STATUS_CLASS_MAP = {
+ 'active': 'primary',
+ 'reserved': 'info',
+ 'deprecated': 'danger',
+ }
+
class Meta:
ordering = ['site', 'group', 'vid']
unique_together = [
@@ -899,7 +947,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
return None
def get_status_class(self):
- return STATUS_CHOICE_CLASSES[self.status]
+ return self.STATUS_CLASS_MAP[self.status]
def get_members(self):
# Return all interfaces assigned to this VLAN
@@ -932,8 +980,9 @@ class Service(ChangeLoggedModel, CustomFieldModel):
name = models.CharField(
max_length=30
)
- protocol = models.PositiveSmallIntegerField(
- choices=IP_PROTOCOL_CHOICES
+ protocol = models.CharField(
+ max_length=50,
+ choices=ServiceProtocolChoices
)
port = models.PositiveIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(65535)],
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
index 29368090e..988eea0f3 100644
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -5,7 +5,7 @@ from netaddr import IPNetwork
from rest_framework import status
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 utilities.testing import APITestCase
@@ -996,13 +996,13 @@ class ServiceTest(APITestCase):
name='Test Device 2', site=site, device_type=devicetype, device_role=devicerole
)
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(
- 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(
- 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):
@@ -1024,7 +1024,7 @@ class ServiceTest(APITestCase):
data = {
'device': self.device1.pk,
'name': 'Test Service 4',
- 'protocol': IP_PROTOCOL_TCP,
+ 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
'port': 4,
}
@@ -1045,19 +1045,19 @@ class ServiceTest(APITestCase):
{
'device': self.device1.pk,
'name': 'Test Service 4',
- 'protocol': IP_PROTOCOL_TCP,
+ 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
'port': 4,
},
{
'device': self.device1.pk,
'name': 'Test Service 5',
- 'protocol': IP_PROTOCOL_TCP,
+ 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
'port': 5,
},
{
'device': self.device1.pk,
'name': 'Test Service 6',
- 'protocol': IP_PROTOCOL_TCP,
+ 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
'port': 6,
},
]
@@ -1076,7 +1076,7 @@ class ServiceTest(APITestCase):
data = {
'device': self.device2.pk,
'name': 'Test Service X',
- 'protocol': IP_PROTOCOL_UDP,
+ 'protocol': ServiceProtocolChoices.PROTOCOL_UDP,
'port': 99,
}
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
index f7f1705ff..fc8b665f7 100644
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -2,7 +2,7 @@ import netaddr
from django.core.exceptions import ValidationError
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
@@ -61,5 +61,5 @@ class TestIPAddress(TestCase):
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
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)
diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py
index e14a257d6..e6780c798 100644
--- a/netbox/ipam/tests/test_views.py
+++ b/netbox/ipam/tests/test_views.py
@@ -5,7 +5,7 @@ from django.test import Client, TestCase
from django.urls import reverse
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 utilities.testing import create_test_user
@@ -264,9 +264,9 @@ class ServiceTestCase(TestCase):
device.save()
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):
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 2cc1a0ea8..362d6173c 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -14,7 +14,7 @@ from utilities.views import (
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
-from .constants import *
+from .choices import *
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).
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(
- [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(
- [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.
@@ -665,8 +665,8 @@ class IPAddressView(PermissionRequiredMixin, View):
'nat_inside', 'interface__device'
)
# 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)
# Related IP table
diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py
index 9adfd84ad..36e37667f 100644
--- a/netbox/utilities/api.py
+++ b/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.viewsets import ModelViewSet as _ModelViewSet, ViewSet
+from utilities.choices import ChoiceSet
from .utils import dict_to_filter_params, dynamic_import
@@ -64,14 +65,17 @@ class ChoiceField(Field):
Represent a ChoiceField as {'value': , 'label': }.
"""
def __init__(self, choices, **kwargs):
+ self.choiceset = choices
self._choices = dict()
+
+ # Unpack grouped choices
for k, v in choices:
- # Unpack grouped choices
if type(v) in [list, tuple]:
for k2, v2 in v:
self._choices[k2] = v2
else:
self._choices[k] = v
+
super().__init__(**kwargs)
def to_representation(self, obj):
@@ -81,6 +85,11 @@ class ChoiceField(Field):
('value', 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
def to_internal_value(self, data):
@@ -104,6 +113,10 @@ class ChoiceField(Field):
try:
if data in self._choices:
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
pass
diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py
new file mode 100644
index 000000000..7738aa7c0
--- /dev/null
+++ b/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)
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
index 011d9e85f..3720fd76d 100644
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -5,7 +5,7 @@ from collections import OrderedDict
from django.core.serializers import serialize
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):
@@ -165,12 +165,18 @@ def to_meters(length, unit):
length = int(length)
if length < 0:
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
- if unit == LENGTH_UNIT_CENTIMETER:
+ if unit == CableLengthUnitChoices.UNIT_CENTIMETER:
return length / 100
- if unit == LENGTH_UNIT_FOOT:
+ if unit == CableLengthUnitChoices.UNIT_FOOT:
return length * 0.3048
- if unit == LENGTH_UNIT_INCH:
+ if unit == CableLengthUnitChoices.UNIT_INCH:
return length * 0.3048 * 12
- raise ValueError("Unknown unit {}. Must be 'm', 'cm', 'ft', or 'in'.".format(unit))
diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py
index 75f36fbb6..8603e31d3 100644
--- a/netbox/virtualization/api/serializers.py
+++ b/netbox/virtualization/api/serializers.py
@@ -3,14 +3,14 @@ from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
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 extras.api.customfields import CustomFieldModelSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN
from tenancy.api.nested_serializers import NestedTenantSerializer
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 .nested_serializers import *
@@ -57,7 +57,7 @@ class ClusterSerializer(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)
cluster = NestedClusterSerializer()
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
@@ -98,8 +98,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
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)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
diff --git a/netbox/virtualization/choices.py b/netbox/virtualization/choices.py
new file mode 100644
index 000000000..d96842c35
--- /dev/null
+++ b/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,
+ }
diff --git a/netbox/virtualization/constants.py b/netbox/virtualization/constants.py
deleted file mode 100644
index 37e9efea2..000000000
--- a/netbox/virtualization/constants.py
+++ /dev/null
@@ -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',
-}
diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py
index a2179bf05..a67296353 100644
--- a/netbox/virtualization/filters.py
+++ b/netbox/virtualization/filters.py
@@ -10,7 +10,7 @@ from tenancy.models import Tenant
from utilities.filters import (
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
)
-from .constants import *
+from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -96,7 +96,7 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdate
label='Search',
)
status = django_filters.MultipleChoiceFilter(
- choices=VM_STATUS_CHOICES,
+ choices=VirtualMachineStatusChoices,
null_value=None
)
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index 8094b0fbe..712c5e1fa 100644
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -2,7 +2,7 @@ from django import forms
from django.core.exceptions import ValidationError
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.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
@@ -15,11 +15,11 @@ from utilities.forms import (
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
SmallTextarea, StaticSelect2, StaticSelect2Multiple
)
-from .constants import *
+from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
VIFACE_TYPE_CHOICES = (
- (IFACE_TYPE_VIRTUAL, 'Virtual'),
+ (InterfaceTypeChoices.TYPE_VIRTUAL, 'Virtual'),
)
@@ -428,7 +428,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class VirtualMachineCSVForm(forms.ModelForm):
status = CSVChoiceField(
- choices=VM_STATUS_CHOICES,
+ choices=VirtualMachineStatusChoices,
required=False,
help_text='Operational status of device'
)
@@ -481,7 +481,7 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
widget=forms.MultipleHiddenInput()
)
status = forms.ChoiceField(
- choices=add_blank_choice(VM_STATUS_CHOICES),
+ choices=add_blank_choice(VirtualMachineStatusChoices),
required=False,
initial='',
widget=StaticSelect2(),
@@ -612,7 +612,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
)
)
status = forms.MultipleChoiceField(
- choices=VM_STATUS_CHOICES,
+ choices=VirtualMachineStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
@@ -717,13 +717,13 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
tagged_vlans = self.cleaned_data['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({
'mode': "An access interface cannot have tagged VLANs assigned."
})
# 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'] = []
@@ -733,7 +733,7 @@ class InterfaceCreateForm(ComponentForm):
)
type = forms.ChoiceField(
choices=VIFACE_TYPE_CHOICES,
- initial=IFACE_TYPE_VIRTUAL,
+ initial=InterfaceTypeChoices.TYPE_VIRTUAL,
widget=forms.HiddenInput()
)
enabled = forms.BooleanField(
@@ -754,7 +754,7 @@ class InterfaceCreateForm(ComponentForm):
required=False
)
mode = forms.ChoiceField(
- choices=add_blank_choice(IFACE_MODE_CHOICES),
+ choices=add_blank_choice(InterfaceModeChoices),
required=False,
widget=StaticSelect2(),
)
@@ -839,7 +839,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
required=False
)
mode = forms.ChoiceField(
- choices=add_blank_choice(IFACE_MODE_CHOICES),
+ choices=add_blank_choice(InterfaceModeChoices),
required=False,
widget=StaticSelect2()
)
@@ -918,7 +918,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
type = forms.ChoiceField(
choices=VIFACE_TYPE_CHOICES,
- initial=IFACE_TYPE_VIRTUAL,
+ initial=InterfaceTypeChoices.TYPE_VIRTUAL,
widget=forms.HiddenInput()
)
enabled = forms.BooleanField(
diff --git a/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py b/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py
new file mode 100644
index 000000000..869975075
--- /dev/null
+++ b/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
+ ),
+
+ ]
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index 630a1468c..f47172f1d 100644
--- a/netbox/virtualization/models.py
+++ b/netbox/virtualization/models.py
@@ -8,7 +8,7 @@ from taggit.managers import TaggableManager
from dcim.models import Device
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
from utilities.models import ChangeLoggedModel
-from .constants import *
+from .choices import *
#
@@ -193,9 +193,10 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
max_length=64,
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'
)
role = models.ForeignKey(
@@ -252,6 +253,12 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
]
+ STATUS_CLASS_MAP = {
+ 'active': 'success',
+ 'offline': 'warning',
+ 'staged': 'primary',
+ }
+
class Meta:
ordering = ['name']
@@ -294,7 +301,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
)
def get_status_class(self):
- return VM_STATUS_CLASSES[self.status]
+ return self.STATUS_CLASS_MAP.get(self.status)
@property
def primary_ip(self):
diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py
index f1e372dd4..683a65a2b 100644
--- a/netbox/virtualization/tests/test_api.py
+++ b/netbox/virtualization/tests/test_api.py
@@ -2,7 +2,7 @@ from django.urls import reverse
from netaddr import IPNetwork
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 ipam.models import IPAddress, VLAN
from utilities.testing import APITestCase
@@ -489,17 +489,17 @@ class InterfaceTest(APITestCase):
self.interface1 = Interface.objects.create(
virtual_machine=self.virtualmachine,
name='Test Interface 1',
- type=IFACE_TYPE_VIRTUAL
+ type=InterfaceTypeChoices.TYPE_VIRTUAL
)
self.interface2 = Interface.objects.create(
virtual_machine=self.virtualmachine,
name='Test Interface 2',
- type=IFACE_TYPE_VIRTUAL
+ type=InterfaceTypeChoices.TYPE_VIRTUAL
)
self.interface3 = Interface.objects.create(
virtual_machine=self.virtualmachine,
name='Test Interface 3',
- type=IFACE_TYPE_VIRTUAL
+ type=InterfaceTypeChoices.TYPE_VIRTUAL
)
self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
@@ -551,7 +551,7 @@ class InterfaceTest(APITestCase):
data = {
'virtual_machine': self.virtualmachine.pk,
'name': 'Test Interface 4',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan3.id,
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
}
@@ -598,21 +598,21 @@ class InterfaceTest(APITestCase):
{
'virtual_machine': self.virtualmachine.pk,
'name': 'Test Interface 4',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
{
'virtual_machine': self.virtualmachine.pk,
'name': 'Test Interface 5',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
{
'virtual_machine': self.virtualmachine.pk,
'name': 'Test Interface 6',
- 'mode': IFACE_MODE_TAGGED,
+ 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},