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], },