From a2a6b754befc9dbd61dc26db6d5deab2db180f52 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Nov 2019 09:32:25 -0500 Subject: [PATCH 01/47] Introduce ChoiceSet class for field choices --- netbox/utilities/api.py | 15 ++++++++++++++- netbox/utilities/choices.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 netbox/utilities/choices.py 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..367451dd5 --- /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) From e09ad6915fd1702b3e06592f3cdf30b601b9f346 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Nov 2019 11:10:46 -0500 Subject: [PATCH 02/47] Circuit.status (#3569) --- netbox/circuits/api/serializers.py | 4 +- netbox/circuits/choices.py | 29 +++++++++++++++ netbox/circuits/constants.py | 16 -------- netbox/circuits/filters.py | 3 +- netbox/circuits/forms.py | 7 ++-- .../migrations/0016_circuit_status_slug.py | 37 +++++++++++++++++++ netbox/circuits/models.py | 19 ++++++++-- netbox/circuits/tests/test_api.py | 13 ++++--- netbox/utilities/choices.py | 2 +- 9 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 netbox/circuits/choices.py create mode 100644 netbox/circuits/migrations/0016_circuit_status_slug.py 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..f562e9837 --- /dev/null +++ b/netbox/circuits/choices.py @@ -0,0 +1,29 @@ +from utilities.choices import ChoiceSet + + +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, + } diff --git a/netbox/circuits/constants.py b/netbox/circuits/constants.py index 9e180e655..7c26de754 100644 --- a/netbox/circuits/constants.py +++ b/netbox/circuits/constants.py @@ -1,19 +1,3 @@ -# 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' diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 088ec144a..a70a3aae2 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -5,6 +5,7 @@ from dcim.models import Region, Site from extras.filters import CustomFieldFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter +from .choices import * from .constants import * from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -84,7 +85,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet): 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..e73504e20 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -9,6 +9,7 @@ from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple ) +from .choices import CircuitStatusChoices from .constants import * from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -194,7 +195,7 @@ class CircuitCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, required=False, help_text='Operational status' ) @@ -235,7 +236,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 +293,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_circuit_status_slug.py b/netbox/circuits/migrations/0016_circuit_status_slug.py new file mode 100644 index 000000000..f4e4dbb3c --- /dev/null +++ b/netbox/circuits/migrations/0016_circuit_status_slug.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.6 on 2019-11-07 03:36 + +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): + + dependencies = [ + ('circuits', '0015_custom_tag_models'), + ] + + operations = [ + 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..abc643db5 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -9,6 +9,7 @@ from dcim.models import CableTermination from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object +from .choices import * from .constants import * @@ -132,9 +133,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 +173,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 +206,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(): diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index e53c2c402..86bf814a8 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,9 +1,10 @@ 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 CircuitStatusChoices +from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z 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 +251,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 +271,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, }, ] diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py index 367451dd5..7738aa7c0 100644 --- a/netbox/utilities/choices.py +++ b/netbox/utilities/choices.py @@ -3,7 +3,7 @@ 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 + # 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): From 62b45494b69987d99b1a02af5d2152dc9a88d7c6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Nov 2019 21:00:34 -0500 Subject: [PATCH 03/47] Convert all DCIM choice classes to ChoiceSets --- netbox/dcim/api/serializers.py | 16 +-- netbox/dcim/choices.py | 200 +++++++++++++++----------------- netbox/dcim/filters.py | 8 +- netbox/dcim/forms.py | 32 ++--- netbox/dcim/models.py | 16 +-- netbox/dcim/tests/test_views.py | 8 +- 6 files changed, 135 insertions(+), 145 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 95ee15bbe..5fb4250f1 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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,7 +238,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() type = ChoiceField( - choices=PowerOutletTypes.CHOICES, + choices=PowerOutletTypeChoices, required=False ) power_port = PowerPortTemplateSerializer( @@ -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,7 +422,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() type = ChoiceField( - choices=PowerOutletTypes.CHOICES, + choices=PowerOutletTypeChoices, required=False ) power_port = NestedPowerPortSerializer( @@ -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) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index c9637965b..b1775184c 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1,3 +1,4 @@ +from utilities.choices import ChoiceSet from .constants import * @@ -5,7 +6,7 @@ from .constants import * # Console port type values # -class ConsolePortTypes: +class ConsolePortTypeChoices(ChoiceSet): """ ConsolePort/ConsoleServerPort.type slugs """ @@ -46,7 +47,7 @@ class ConsolePortTypes: # Power port types # -class PowerPortTypes: +class PowerPortTypeChoices(ChoiceSet): # TODO: Add more power port types # IEC 60320 TYPE_IEC_C6 = 'iec-60320-c6' @@ -133,7 +134,7 @@ class PowerPortTypes: # Power outlet types # -class PowerOutletTypes: +class PowerOutletTypeChoices(ChoiceSet): # TODO: Add more power outlet types # IEC 60320 TYPE_IEC_C5 = 'iec-60320-c5' @@ -220,7 +221,7 @@ class PowerOutletTypes: # Interface type values # -class InterfaceTypes: +class InterfaceTypeChoices(ChoiceSet): """ Interface.type slugs """ @@ -315,7 +316,7 @@ class InterfaceTypes: # Other TYPE_OTHER = 'other' - TYPE_CHOICES = ( + CHOICES = ( ( 'Virtual interfaces', ( @@ -444,90 +445,85 @@ 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, + } # # Port type values # -class PortTypes: +class PortTypeChoices(ChoiceSet): """ FrontPort/RearPort.type slugs """ @@ -545,7 +541,7 @@ class PortTypes: TYPE_LSH = 'lsh' TYPE_LSH_APC = 'lsh-apc' - TYPE_CHOICES = ( + CHOICES = ( ( 'Copper', ( @@ -571,24 +567,18 @@ 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, + } diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index dc98282fb..10a7dae96 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -643,7 +643,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( @@ -659,7 +659,7 @@ class ConsolePortFilter(DeviceComponentFilterSet): class ConsoleServerPortFilter(DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( - choices=ConsolePortTypes.CHOICES, + choices=ConsolePortTypeChoices, null_value=None ) cabled = django_filters.BooleanFilter( @@ -675,7 +675,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet): class PowerPortFilter(DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( - choices=PowerPortTypes.CHOICES, + choices=PowerPortTypeChoices, null_value=None ) cabled = django_filters.BooleanFilter( @@ -691,7 +691,7 @@ class PowerPortFilter(DeviceComponentFilterSet): class PowerOutletFilter(DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( - choices=PowerOutletTypes.CHOICES, + choices=PowerOutletTypeChoices, null_value=None ) cabled = django_filters.BooleanFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index abf9b7b1b..bf9bc90a1 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -954,7 +954,7 @@ class ConsolePortTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=ConsolePortTypes.CHOICES, + choices=ConsolePortTypeChoices, widget=StaticSelect2() ) @@ -976,7 +976,7 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=add_blank_choice(ConsolePortTypes.CHOICES), + choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) @@ -998,7 +998,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( @@ -1040,7 +1040,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( @@ -1307,7 +1307,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm): class InterfaceTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( - choices=InterfaceTypes.TYPE_CHOICES + choices=InterfaceTypeChoices.CHOICES ) class Meta: @@ -1319,12 +1319,12 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): def clean_type(self): # Convert slug value to field integer value slug = self.cleaned_data['type'] - return InterfaceTypes.slug_to_integer(slug) + return InterfaceTypeChoices.slug_to_id(slug) class FrontPortTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( - choices=PortTypes.TYPE_CHOICES + choices=PortTypeChoices.CHOICES ) rear_port = forms.ModelChoiceField( queryset=RearPortTemplate.objects.all(), @@ -1341,12 +1341,12 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm): def clean_type(self): # Convert slug value to field integer value slug = self.cleaned_data['type'] - return PortTypes.slug_to_integer(slug) + return PortTypeChoices.slug_to_id(slug) class RearPortTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( - choices=PortTypes.TYPE_CHOICES + choices=PortTypeChoices.CHOICES ) class Meta: @@ -1358,7 +1358,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm): def clean_type(self): # Convert slug value to field integer value slug = self.cleaned_data['type'] - return PortTypes.slug_to_integer(slug) + return PortTypeChoices.slug_to_id(slug) class DeviceBayTemplateImportForm(ComponentTemplateImportForm): @@ -2083,7 +2083,7 @@ class ConsolePortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=add_blank_choice(ConsolePortTypes.CHOICES), + choices=add_blank_choice(ConsolePortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2120,7 +2120,7 @@ class ConsoleServerPortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=add_blank_choice(ConsolePortTypes.CHOICES), + choices=add_blank_choice(ConsolePortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2139,7 +2139,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() ) @@ -2192,7 +2192,7 @@ class PowerPortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=add_blank_choice(PowerPortTypes.CHOICES), + choices=add_blank_choice(PowerPortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2252,7 +2252,7 @@ class PowerOutletCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=add_blank_choice(PowerOutletTypes.CHOICES), + choices=add_blank_choice(PowerOutletTypeChoices), required=False, widget=StaticSelect2() ) @@ -2286,7 +2286,7 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=PowerOutletTypes.CHOICES, + choices=PowerOutletTypeChoices, required=False ) feed_leg = forms.ChoiceField( diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6fd008d0a..d582be33c 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1017,7 +1017,7 @@ class ConsolePortTemplate(ComponentTemplateModel): ) type = models.CharField( max_length=50, - choices=ConsolePortTypes.CHOICES, + choices=ConsolePortTypeChoices, blank=True ) @@ -1052,7 +1052,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): ) type = models.CharField( max_length=50, - choices=ConsolePortTypes.CHOICES, + choices=ConsolePortTypeChoices, blank=True ) @@ -1087,7 +1087,7 @@ class PowerPortTemplate(ComponentTemplateModel): ) type = models.CharField( max_length=50, - choices=PowerPortTypes.CHOICES, + choices=PowerPortTypeChoices, blank=True ) maximum_draw = models.PositiveSmallIntegerField( @@ -1135,7 +1135,7 @@ class PowerOutletTemplate(ComponentTemplateModel): ) type = models.CharField( max_length=50, - choices=PowerOutletTypes.CHOICES, + choices=PowerOutletTypeChoices, blank=True ) power_port = models.ForeignKey( @@ -1871,7 +1871,7 @@ class ConsolePort(CableTermination, ComponentModel): ) type = models.CharField( max_length=50, - choices=ConsolePortTypes.CHOICES, + choices=ConsolePortTypeChoices, blank=True ) connected_endpoint = models.OneToOneField( @@ -1928,7 +1928,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): ) type = models.CharField( max_length=50, - choices=ConsolePortTypes.CHOICES, + choices=ConsolePortTypeChoices, blank=True ) connection_status = models.NullBooleanField( @@ -1977,7 +1977,7 @@ class PowerPort(CableTermination, ComponentModel): ) type = models.CharField( max_length=50, - choices=PowerPortTypes.CHOICES, + choices=PowerPortTypeChoices, blank=True ) maximum_draw = models.PositiveSmallIntegerField( @@ -2120,7 +2120,7 @@ class PowerOutlet(CableTermination, ComponentModel): ) type = models.CharField( max_length=50, - choices=PowerOutletTypes.CHOICES, + choices=PowerOutletTypeChoices, blank=True ) power_port = models.ForeignKey( diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 74457af0e..c556a6831 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -326,22 +326,22 @@ 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) From 8d27b62114c582bf8e5ae268945b448c33509e9f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Nov 2019 21:28:34 -0500 Subject: [PATCH 04/47] Rack.status to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 30 ++++++++++++++++- netbox/dcim/constants.py | 14 -------- netbox/dcim/forms.py | 4 +-- .../0078_rack_choicefields_to_slugs.py | 33 +++++++++++++++++++ netbox/dcim/models.py | 6 ++-- 6 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5fb4250f1..d92b87a63 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -117,7 +117,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False) role = NestedRackRoleSerializer(required=False, allow_null=True) - type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) + type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False) tags = TagListSerializerField(required=False) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index b1775184c..32bc4e75d 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1,5 +1,33 @@ from utilities.choices import ChoiceSet -from .constants import * + + +# +# 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, + } # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 2e2285b14..27ca8d48a 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,17 +1,3 @@ -# 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'), -) - # Rack widths RACK_WIDTH_19IN = 19 RACK_WIDTH_23IN = 23 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index bf9bc90a1..79a2659e5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -487,7 +487,7 @@ class RackCSVForm(forms.ModelForm): } ) type = CSVChoiceField( - choices=RACK_TYPE_CHOICES, + choices=RackTypeChoices, required=False, help_text='Rack type' ) @@ -593,7 +593,7 @@ 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() ) diff --git a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py new file mode 100644 index 000000000..c59ec0be9 --- /dev/null +++ b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py @@ -0,0 +1,33 @@ +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'), +) + + +def rack_type_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_TYPE_CHOICES: + Rack.objects.filter(status=str(id)).update(status=slug) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0077_power_types'), + ] + + operations = [ + migrations.AlterField( + model_name='rack', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=rack_type_to_slug + ) + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index d582be33c..27814cd7f 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -497,10 +497,10 @@ 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( From 07aa036fe8fb8e6b1eb925313963f3a139e1e80d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Nov 2019 21:33:56 -0500 Subject: [PATCH 05/47] Convert RACK_WIDTH_CHOICES to ChoiceSet --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 11 +++++++++++ netbox/dcim/constants.py | 8 -------- netbox/dcim/forms.py | 7 ++----- netbox/dcim/models.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d92b87a63..67450d7b1 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -118,7 +118,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False) role = NestedRackRoleSerializer(required=False, allow_null=True) type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True) - width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) + width = ChoiceField(choices=RackWidthChoices, required=False) outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False) tags = TagListSerializerField(required=False) device_count = serializers.IntegerField(read_only=True) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 32bc4e75d..c75568ee8 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -30,6 +30,17 @@ class RackTypeChoices(ChoiceSet): } +class RackWidthChoices(ChoiceSet): + + WIDTH_19IN = 19 + WIDTH_23IN = 23 + + CHOICES = ( + (WIDTH_19IN, '19 inches'), + (WIDTH_23IN, '23 inches'), + ) + + # # Console port type values # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 27ca8d48a..c6f4ced5f 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,11 +1,3 @@ -# 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 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 79a2659e5..6a532741d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -492,10 +492,7 @@ class RackCSVForm(forms.ModelForm): 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( @@ -598,7 +595,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor widget=StaticSelect2() ) width = forms.ChoiceField( - choices=add_blank_choice(RACK_WIDTH_CHOICES), + choices=add_blank_choice(RackWidthChoices), required=False, widget=StaticSelect2() ) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 27814cd7f..5f0de7c0d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -504,8 +504,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): 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' ) From e1e09bff9b15c512e67c7cab82b8c7be7c112ef8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Nov 2019 21:50:33 -0500 Subject: [PATCH 06/47] Correct Rack.type migration logic --- netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py index c59ec0be9..2e5131c7a 100644 --- a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py @@ -12,7 +12,7 @@ RACK_TYPE_CHOICES = ( def rack_type_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') for id, slug in RACK_TYPE_CHOICES: - Rack.objects.filter(status=str(id)).update(status=slug) + Rack.objects.filter(type=str(id)).update(type=slug) class Migration(migrations.Migration): From c79c29e7692483f64f5375a0634cc44d5369a98a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Nov 2019 22:03:41 -0500 Subject: [PATCH 07/47] Rack.status to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 25 ++++++++++++++++++ netbox/dcim/constants.py | 16 +----------- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 6 ++--- .../0078_rack_choicefields_to_slugs.py | 26 ++++++++++++++++++- netbox/dcim/models.py | 17 +++++++++--- 7 files changed, 69 insertions(+), 25 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 67450d7b1..c696e4d2a 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -115,7 +115,7 @@ 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=RackTypeChoices, required=False, allow_null=True) width = ChoiceField(choices=RackWidthChoices, required=False) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index c75568ee8..4ebeac047 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -41,6 +41,31 @@ class RackWidthChoices(ChoiceSet): ) +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, + } + + # # Console port type values # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index c6f4ced5f..6a6e9e521 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,4 +1,4 @@ -# Rack faces +# Device rack faces RACK_FACE_FRONT = 0 RACK_FACE_REAR = 1 RACK_FACE_CHOICES = [ @@ -6,20 +6,6 @@ RACK_FACE_CHOICES = [ [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 diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 10a7dae96..aa5113ff1 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -147,7 +147,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet): label='Group', ) status = django_filters.MultipleChoiceFilter( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, null_value=None ) role_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6a532741d..b487fb0a6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -473,7 +473,7 @@ class RackCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, required=False, help_text='Operational status' ) @@ -568,7 +568,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() @@ -662,7 +662,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ) ) status = forms.MultipleChoiceField( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py index 2e5131c7a..7c9b32310 100644 --- a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py @@ -8,6 +8,14 @@ RACK_TYPE_CHOICES = ( (1100, 'wall-cabinet'), ) +RACK_STATUS_CHOICES = ( + (0, 'reserved'), + (1, 'available'), + (2, 'planned'), + (3, 'active'), + (4, 'deprecated'), +) + def rack_type_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') @@ -15,6 +23,12 @@ def rack_type_to_slug(apps, schema_editor): 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) + + class Migration(migrations.Migration): dependencies = [ @@ -22,6 +36,7 @@ class Migration(migrations.Migration): ] operations = [ + # Rack.type migrations.AlterField( model_name='rack', name='type', @@ -29,5 +44,14 @@ class Migration(migrations.Migration): ), migrations.RunPython( code=rack_type_to_slug - ) + ), + # Rack.status + migrations.AlterField( + model_name='rack', + name='status', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=rack_status_to_slug + ), ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 5f0de7c0d..23532efa8 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -473,9 +473,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', @@ -552,6 +553,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 = [ @@ -644,7 +653,7 @@ 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): """ From a8db07e0a849b2572ab9004619ac4eacee255467 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 16 Nov 2019 21:46:07 -0500 Subject: [PATCH 08/47] Device.face to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 20 ++++++++++++ netbox/dcim/constants.py | 14 --------- netbox/dcim/fixtures/dcim.json | 22 ++++++------- netbox/dcim/forms.py | 2 +- .../0078_rack_choicefields_to_slugs.py | 2 +- .../0079_device_choicefields_to_slugs.py | 31 +++++++++++++++++++ netbox/dcim/models.py | 16 +++++----- netbox/dcim/tests/test_forms.py | 8 ++--- netbox/dcim/tests/test_models.py | 6 ++-- 10 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 netbox/dcim/migrations/0079_device_choicefields_to_slugs.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c696e4d2a..71b53e8d4 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -324,7 +324,7 @@ 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) + face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 4ebeac047..819b1678f 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -66,6 +66,26 @@ class RackStatusChoices(ChoiceSet): } +# +# 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, + } + + # # Console port type values # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 6a6e9e521..c25502bcb 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,17 +1,3 @@ -# Device rack faces -RACK_FACE_FRONT = 0 -RACK_FACE_REAR = 1 -RACK_FACE_CHOICES = [ - [RACK_FACE_FRONT, 'Front'], - [RACK_FACE_REAR, 'Rear'], -] - -# 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 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 b487fb0a6..aec0769d6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1716,7 +1716,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' ) diff --git a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py index 7c9b32310..274b23812 100644 --- a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py @@ -49,7 +49,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='rack', name='status', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(blank=True, default='active', max_length=50), ), migrations.RunPython( code=rack_status_to_slug diff --git a/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py b/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py new file mode 100644 index 000000000..bc8785dae --- /dev/null +++ b/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py @@ -0,0 +1,31 @@ +from django.db import migrations, models + +DEVICE_FACE_CHOICES = ( + (0, 'front'), + (1, 'rear'), +) + + +def rack_type_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) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0078_rack_choicefields_to_slugs'), + ] + + operations = [ + # Device.face + migrations.AlterField( + model_name='device', + name='face', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=rack_type_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 23532efa8..42b10a102 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -655,7 +655,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def get_status_class(self): 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. @@ -687,10 +687,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()): """ @@ -1535,10 +1535,10 @@ 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( @@ -1634,7 +1634,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.", }) @@ -1644,7 +1644,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.", }) diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 2f333ea69..8ac9aa84e 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -21,7 +21,7 @@ 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, @@ -38,7 +38,7 @@ 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, @@ -54,7 +54,7 @@ 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, @@ -71,7 +71,7 @@ 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, diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 2b5bed283..5d1099029 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) From 88b8cf33603a581948bd8e237a8f42355c36903f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Nov 2019 20:54:21 -0500 Subject: [PATCH 09/47] Tweak migrations to handle NULL values --- netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py | 2 +- netbox/dcim/migrations/0079_device_choicefields_to_slugs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py index 274b23812..b20cf23e8 100644 --- a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py @@ -40,7 +40,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='rack', name='type', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(blank=True, default='', max_length=50), ), migrations.RunPython( code=rack_type_to_slug diff --git a/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py b/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py index bc8785dae..572c1866c 100644 --- a/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='face', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(blank=True, default='', max_length=50), ), migrations.RunPython( code=rack_type_to_slug From 4f0d82ac168913b4a6289b2740dbf7cc309161a2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Nov 2019 21:04:12 -0500 Subject: [PATCH 10/47] Standardize migration names for #3569 --- ...uit_status_slug.py => 0016_circuit_choicefields_to_slugs.py} | 2 -- 1 file changed, 2 deletions(-) rename netbox/circuits/migrations/{0016_circuit_status_slug.py => 0016_circuit_choicefields_to_slugs.py} (94%) diff --git a/netbox/circuits/migrations/0016_circuit_status_slug.py b/netbox/circuits/migrations/0016_circuit_choicefields_to_slugs.py similarity index 94% rename from netbox/circuits/migrations/0016_circuit_status_slug.py rename to netbox/circuits/migrations/0016_circuit_choicefields_to_slugs.py index f4e4dbb3c..5232abe26 100644 --- a/netbox/circuits/migrations/0016_circuit_status_slug.py +++ b/netbox/circuits/migrations/0016_circuit_choicefields_to_slugs.py @@ -1,5 +1,3 @@ -# Generated by Django 2.2.6 on 2019-11-07 03:36 - from django.db import migrations, models From fbd12e1887560bc027e136f619f142f6be516435 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Nov 2019 21:14:43 -0500 Subject: [PATCH 11/47] Create a separate migration for each field --- ...lugs.py => 0016_circuit_status_to_slug.py} | 3 +- ..._to_slugs.py => 0078_rack_type_to_slug.py} | 25 +------------- .../migrations/0079_rack_status_to_slug.py | 34 +++++++++++++++++++ ...o_slugs.py => 0080_device_face_to_slug.py} | 8 ++--- 4 files changed, 41 insertions(+), 29 deletions(-) rename netbox/circuits/migrations/{0016_circuit_choicefields_to_slugs.py => 0016_circuit_status_to_slug.py} (96%) rename netbox/dcim/migrations/{0078_rack_choicefields_to_slugs.py => 0078_rack_type_to_slug.py} (55%) create mode 100644 netbox/dcim/migrations/0079_rack_status_to_slug.py rename netbox/dcim/migrations/{0079_device_choicefields_to_slugs.py => 0080_device_face_to_slug.py} (78%) diff --git a/netbox/circuits/migrations/0016_circuit_choicefields_to_slugs.py b/netbox/circuits/migrations/0016_circuit_status_to_slug.py similarity index 96% rename from netbox/circuits/migrations/0016_circuit_choicefields_to_slugs.py rename to netbox/circuits/migrations/0016_circuit_status_to_slug.py index 5232abe26..f70698c23 100644 --- a/netbox/circuits/migrations/0016_circuit_choicefields_to_slugs.py +++ b/netbox/circuits/migrations/0016_circuit_status_to_slug.py @@ -18,6 +18,7 @@ def circuit_status_to_slug(apps, schema_editor): class Migration(migrations.Migration): + atomic = False dependencies = [ ('circuits', '0015_custom_tag_models'), @@ -31,5 +32,5 @@ class Migration(migrations.Migration): ), migrations.RunPython( code=circuit_status_to_slug - ) + ), ] diff --git a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py b/netbox/dcim/migrations/0078_rack_type_to_slug.py similarity index 55% rename from netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py rename to netbox/dcim/migrations/0078_rack_type_to_slug.py index b20cf23e8..33c9c8f7b 100644 --- a/netbox/dcim/migrations/0078_rack_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0078_rack_type_to_slug.py @@ -8,14 +8,6 @@ RACK_TYPE_CHOICES = ( (1100, 'wall-cabinet'), ) -RACK_STATUS_CHOICES = ( - (0, 'reserved'), - (1, 'available'), - (2, 'planned'), - (3, 'active'), - (4, 'deprecated'), -) - def rack_type_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') @@ -23,20 +15,14 @@ def rack_type_to_slug(apps, schema_editor): 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) - - class Migration(migrations.Migration): + atomic = False dependencies = [ ('dcim', '0077_power_types'), ] operations = [ - # Rack.type migrations.AlterField( model_name='rack', name='type', @@ -45,13 +31,4 @@ class Migration(migrations.Migration): migrations.RunPython( code=rack_type_to_slug ), - # Rack.status - migrations.AlterField( - model_name='rack', - name='status', - field=models.CharField(blank=True, default='active', max_length=50), - ), - migrations.RunPython( - code=rack_status_to_slug - ), ] diff --git a/netbox/dcim/migrations/0079_rack_status_to_slug.py b/netbox/dcim/migrations/0079_rack_status_to_slug.py new file mode 100644 index 000000000..1760c321e --- /dev/null +++ b/netbox/dcim/migrations/0079_rack_status_to_slug.py @@ -0,0 +1,34 @@ +from django.db import migrations, models + +RACK_STATUS_CHOICES = ( + (0, 'reserved'), + (1, 'available'), + (2, 'planned'), + (3, 'active'), + (4, 'deprecated'), +) + + +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) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0078_rack_type_to_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='rack', + name='status', + field=models.CharField(blank=True, default='active', max_length=50), + ), + migrations.RunPython( + code=rack_status_to_slug + ), + ] diff --git a/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py b/netbox/dcim/migrations/0080_device_face_to_slug.py similarity index 78% rename from netbox/dcim/migrations/0079_device_choicefields_to_slugs.py rename to netbox/dcim/migrations/0080_device_face_to_slug.py index 572c1866c..93d9c7609 100644 --- a/netbox/dcim/migrations/0079_device_choicefields_to_slugs.py +++ b/netbox/dcim/migrations/0080_device_face_to_slug.py @@ -6,26 +6,26 @@ DEVICE_FACE_CHOICES = ( ) -def rack_type_to_slug(apps, schema_editor): +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) class Migration(migrations.Migration): + atomic = False dependencies = [ - ('dcim', '0078_rack_choicefields_to_slugs'), + ('dcim', '0079_rack_status_to_slug'), ] operations = [ - # Device.face migrations.AlterField( model_name='device', name='face', field=models.CharField(blank=True, default='', max_length=50), ), migrations.RunPython( - code=rack_type_to_slug + code=device_face_to_slug ), ] From afd82fd9d3e1f7ea5f7a851a1eef1af1ef35bc36 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Nov 2019 22:03:25 -0500 Subject: [PATCH 12/47] DeviceType.subdevice_role to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 20 ++++++++++++ netbox/dcim/constants.py | 9 ------ netbox/dcim/forms.py | 10 +++--- .../0081_devicetype_subdevicerole_to_slug.py | 31 +++++++++++++++++++ netbox/dcim/models.py | 22 +++++++------ netbox/dcim/tables.py | 8 ----- netbox/dcim/tests/test_api.py | 5 +-- 8 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 71b53e8d4..11450250c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 819b1678f..58504ab34 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -66,6 +66,26 @@ class RackStatusChoices(ChoiceSet): } +# +# 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 # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index c25502bcb..bbe8d7c5e 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,12 +1,3 @@ -# 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 # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index aec0769d6..5ab66bc47 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -879,12 +879,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, @@ -3382,7 +3380,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) diff --git a/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py b/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py new file mode 100644 index 000000000..edcfc9e4b --- /dev/null +++ b/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py @@ -0,0 +1,31 @@ +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', '0080_device_face_to_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=devicetype_subdevicerole_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 42b10a102..93aa8c8fb 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -919,12 +919,12 @@ 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, 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 @@ -968,7 +968,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, ) @@ -988,13 +988,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." }) @@ -1005,11 +1007,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): diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index a8a4fc227..a5edd1f06 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..2963f7329 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 SubdeviceRoleChoices from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -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' From 4e2863e4ec66026c19b4274b849b7d73d500874d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Nov 2019 21:28:59 -0500 Subject: [PATCH 13/47] Move CircuitTermination.term_side choices to a ChoiceSet --- netbox/circuits/choices.py | 19 ++++++++++++++++++ netbox/circuits/constants.py | 7 ------- netbox/circuits/filters.py | 1 - netbox/circuits/forms.py | 1 - netbox/circuits/models.py | 5 ++--- netbox/circuits/tests/test_api.py | 32 ++++++++++++++++++++++--------- netbox/circuits/views.py | 14 +++++++++----- 7 files changed, 53 insertions(+), 26 deletions(-) delete mode 100644 netbox/circuits/constants.py diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py index f562e9837..94a765d11 100644 --- a/netbox/circuits/choices.py +++ b/netbox/circuits/choices.py @@ -1,6 +1,10 @@ from utilities.choices import ChoiceSet +# +# Circuits +# + class CircuitStatusChoices(ChoiceSet): STATUS_DEPROVISIONING = 'deprovisioning' @@ -27,3 +31,18 @@ class CircuitStatusChoices(ChoiceSet): 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 7c26de754..000000000 --- a/netbox/circuits/constants.py +++ /dev/null @@ -1,7 +0,0 @@ -# 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 a70a3aae2..b4ef655f5 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -6,7 +6,6 @@ from extras.filters import CustomFieldFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .choices import * -from .constants import * from .models import Circuit, CircuitTermination, CircuitType, Provider diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index e73504e20..ad99dd40d 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -10,7 +10,6 @@ from utilities.forms import ( FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple ) from .choices import CircuitStatusChoices -from .constants import * from .models import Circuit, CircuitTermination, CircuitType, Provider diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index abc643db5..5f80a4bfe 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -3,14 +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 .choices import * -from .constants import * class Provider(ChangeLoggedModel, CustomFieldModel): @@ -231,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 86bf814a8..c6100d825 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,8 +1,7 @@ from django.urls import reverse from rest_framework import status -from circuits.choices import CircuitStatusChoices -from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z +from circuits.choices import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.models import Site from extras.constants import GRAPH_TYPE_PROVIDER @@ -337,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): @@ -367,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, } @@ -386,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) From 180d3d002992d6ce0c2cabe4905fc5ef0a9e8b35 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Nov 2019 21:44:30 -0500 Subject: [PATCH 14/47] Resolved migration discrepancies when dealing with NULL values --- netbox/dcim/migrations/0078_rack_type_to_slug.py | 5 +++++ netbox/dcim/migrations/0079_rack_status_to_slug.py | 2 +- netbox/dcim/migrations/0080_device_face_to_slug.py | 5 +++++ .../dcim/migrations/0081_devicetype_subdevicerole_to_slug.py | 5 +++++ netbox/dcim/models.py | 1 + 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/migrations/0078_rack_type_to_slug.py b/netbox/dcim/migrations/0078_rack_type_to_slug.py index 33c9c8f7b..2ebcd7168 100644 --- a/netbox/dcim/migrations/0078_rack_type_to_slug.py +++ b/netbox/dcim/migrations/0078_rack_type_to_slug.py @@ -31,4 +31,9 @@ class Migration(migrations.Migration): migrations.RunPython( code=rack_type_to_slug ), + migrations.AlterField( + model_name='rack', + name='type', + field=models.CharField(blank=True, max_length=50), + ), ] diff --git a/netbox/dcim/migrations/0079_rack_status_to_slug.py b/netbox/dcim/migrations/0079_rack_status_to_slug.py index 1760c321e..76890d796 100644 --- a/netbox/dcim/migrations/0079_rack_status_to_slug.py +++ b/netbox/dcim/migrations/0079_rack_status_to_slug.py @@ -26,7 +26,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='rack', name='status', - field=models.CharField(blank=True, default='active', max_length=50), + field=models.CharField(default='active', max_length=50), ), migrations.RunPython( code=rack_status_to_slug diff --git a/netbox/dcim/migrations/0080_device_face_to_slug.py b/netbox/dcim/migrations/0080_device_face_to_slug.py index 93d9c7609..4fe214770 100644 --- a/netbox/dcim/migrations/0080_device_face_to_slug.py +++ b/netbox/dcim/migrations/0080_device_face_to_slug.py @@ -28,4 +28,9 @@ class Migration(migrations.Migration): migrations.RunPython( code=device_face_to_slug ), + migrations.AlterField( + model_name='device', + name='face', + field=models.CharField(blank=True, max_length=50), + ), ] diff --git a/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py b/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py index edcfc9e4b..2497f1702 100644 --- a/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py +++ b/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py @@ -28,4 +28,9 @@ class Migration(migrations.Migration): 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/models.py b/netbox/dcim/models.py index 93aa8c8fb..4b4087d9a 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -922,6 +922,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): subdevice_role = models.CharField( max_length=50, choices=SubdeviceRoleChoices, + blank=True, verbose_name='Parent/child status', help_text='Parent devices house child devices in device bays. Leave blank ' 'if this device type is neither a parent nor a child.' From 5f5081f71909ddaffc2d9ce25223a8b63a0c50ba Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Nov 2019 22:11:02 -0500 Subject: [PATCH 15/47] Interface.type to slug (#3569) --- netbox/dcim/api/serializers.py | 4 +- netbox/dcim/choices.py | 10 +- netbox/dcim/constants.py | 231 +----------------- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 26 +- .../migrations/0082_interface_type_to_slug.py | 114 +++++++++ netbox/dcim/models.py | 18 +- netbox/dcim/tests/test_api.py | 22 +- netbox/dcim/tests/test_models.py | 8 +- netbox/dcim/tests/test_views.py | 14 +- netbox/virtualization/api/serializers.py | 5 +- netbox/virtualization/forms.py | 9 +- netbox/virtualization/tests/test_api.py | 9 +- 13 files changed, 189 insertions(+), 283 deletions(-) create mode 100644 netbox/dcim/migrations/0082_interface_type_to_slug.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 11450250c..7018a8fc9 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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 @@ -467,7 +467,7 @@ 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) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 58504ab34..12df4f701 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -107,7 +107,7 @@ class DeviceFaceChoices(ChoiceSet): # -# Console port type values +# ConsolePorts # class ConsolePortTypeChoices(ChoiceSet): @@ -148,7 +148,7 @@ class ConsolePortTypeChoices(ChoiceSet): # -# Power port types +# PowerPorts # class PowerPortTypeChoices(ChoiceSet): @@ -235,7 +235,7 @@ class PowerPortTypeChoices(ChoiceSet): # -# Power outlet types +# PowerOutlets # class PowerOutletTypeChoices(ChoiceSet): @@ -322,7 +322,7 @@ class PowerOutletTypeChoices(ChoiceSet): # -# Interface type values +# Interfaces # class InterfaceTypeChoices(ChoiceSet): @@ -624,7 +624,7 @@ class InterfaceTypeChoices(ChoiceSet): # -# Port type values +# FrontPorts/RearPorts # class PortTypeChoices(ChoiceSet): diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index bbe8d7c5e..4b199cf15 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,230 +1,21 @@ +from .choices import InterfaceTypeChoices + + # -# 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 diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index aa5113ff1..610a05bd9 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -748,7 +748,7 @@ class InterfaceFilter(django_filters.FilterSet): label='Assigned VID' ) type = django_filters.MultipleChoiceFilter( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, null_value=None ) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 5ab66bc47..0a4444907 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1076,7 +1076,7 @@ class InterfaceTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2() ) mgmt_only = forms.BooleanField( @@ -1091,7 +1091,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() ) @@ -1311,11 +1311,6 @@ 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 InterfaceTypeChoices.slug_to_id(slug) - class FrontPortTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( @@ -2031,7 +2026,7 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2() ) enabled = forms.BooleanField( @@ -2377,12 +2372,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 @@ -2421,7 +2418,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): label='Name' ) type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2(), ) enabled = forms.BooleanField( @@ -2490,7 +2487,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() @@ -2532,7 +2530,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() ) @@ -2602,7 +2600,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 = [] diff --git a/netbox/dcim/migrations/0082_interface_type_to_slug.py b/netbox/dcim/migrations/0082_interface_type_to_slug.py new file mode 100644 index 000000000..50885d461 --- /dev/null +++ b/netbox/dcim/migrations/0082_interface_type_to_slug.py @@ -0,0 +1,114 @@ +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'), +) + + +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) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0081_devicetype_subdevicerole_to_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='interfacetemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interfacetemplate_type_to_slug + ), + migrations.AlterField( + model_name='interface', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interface_type_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 4b4087d9a..4e67f61cc 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1206,9 +1206,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, @@ -2238,9 +2238,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 @@ -2323,7 +2323,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." }) @@ -2352,7 +2352,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()]) @@ -2435,7 +2435,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): diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 2963f7329..730639704 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2554,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 = { @@ -2842,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'), @@ -3411,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') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 5d1099029..fae6a589d 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -193,7 +193,7 @@ class DeviceTestCase(TestCase): InterfaceTemplate( device_type=self.device_type, name='Interface 1', - type=IFACE_TYPE_1GE_FIXED, + type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ).save() @@ -257,7 +257,7 @@ class DeviceTestCase(TestCase): Interface.objects.get( device=d, name='Interface 1', - type=IFACE_TYPE_1GE_FIXED, + type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ) @@ -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() diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index c556a6831..6bda7c6c5 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -348,7 +348,7 @@ device-bays: 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,17 +514,17 @@ 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() diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 75f36fbb6..4b519e5e2 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -3,7 +3,8 @@ 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 InterfaceTypeChoices +from dcim.constants import IFACE_MODE_CHOICES from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer @@ -98,7 +99,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): virtual_machine = NestedVirtualMachineSerializer() - type = ChoiceField(choices=IFACE_TYPE_CHOICES, default=IFACE_TYPE_VIRTUAL, required=False) + type = ChoiceField(choices=InterfaceTypeChoices, default=InterfaceTypeChoices.TYPE_VIRTUAL, required=False) mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 8094b0fbe..09848e0c7 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -2,7 +2,8 @@ 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 InterfaceTypeChoices +from dcim.constants import IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL, IFACE_MODE_CHOICES 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 @@ -19,7 +20,7 @@ from .constants import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine VIFACE_TYPE_CHOICES = ( - (IFACE_TYPE_VIRTUAL, 'Virtual'), + (InterfaceTypeChoices.TYPE_VIRTUAL, 'Virtual'), ) @@ -733,7 +734,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( @@ -918,7 +919,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/tests/test_api.py b/netbox/virtualization/tests/test_api.py index f1e372dd4..64d9cf8b9 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -2,7 +2,8 @@ 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 InterfaceTypeChoices +from dcim.constants import IFACE_MODE_TAGGED from dcim.models import Interface from ipam.models import IPAddress, VLAN from utilities.testing import APITestCase @@ -489,17 +490,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) From f93cd17fee21aa07bba8c40581d403ed45acca5b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Nov 2019 22:26:35 -0500 Subject: [PATCH 16/47] Consolidate #3569 field migrations by model --- ...to_slug.py => 0016_3569_circuit_fields.py} | 0 .../dcim/migrations/0078_3569_site_fields.py | 12 +++++++ ...pe_to_slug.py => 0079_3569_rack_fields.py} | 28 ++++++++++++++- .../migrations/0079_rack_status_to_slug.py | 34 ------------------- ...slug.py => 0080_3569_devicetype_fields.py} | 2 +- ..._to_slug.py => 0081_3569_device_fields.py} | 2 +- ..._slug.py => 0082_3569_interface_fields.py} | 2 +- 7 files changed, 42 insertions(+), 38 deletions(-) rename netbox/circuits/migrations/{0016_circuit_status_to_slug.py => 0016_3569_circuit_fields.py} (100%) create mode 100644 netbox/dcim/migrations/0078_3569_site_fields.py rename netbox/dcim/migrations/{0078_rack_type_to_slug.py => 0079_3569_rack_fields.py} (58%) delete mode 100644 netbox/dcim/migrations/0079_rack_status_to_slug.py rename netbox/dcim/migrations/{0081_devicetype_subdevicerole_to_slug.py => 0080_3569_devicetype_fields.py} (95%) rename netbox/dcim/migrations/{0080_device_face_to_slug.py => 0081_3569_device_fields.py} (94%) rename netbox/dcim/migrations/{0082_interface_type_to_slug.py => 0082_3569_interface_fields.py} (98%) diff --git a/netbox/circuits/migrations/0016_circuit_status_to_slug.py b/netbox/circuits/migrations/0016_3569_circuit_fields.py similarity index 100% rename from netbox/circuits/migrations/0016_circuit_status_to_slug.py rename to netbox/circuits/migrations/0016_3569_circuit_fields.py 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..0826f7f78 --- /dev/null +++ b/netbox/dcim/migrations/0078_3569_site_fields.py @@ -0,0 +1,12 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0077_power_types'), + ] + + operations = [ + ] diff --git a/netbox/dcim/migrations/0078_rack_type_to_slug.py b/netbox/dcim/migrations/0079_3569_rack_fields.py similarity index 58% rename from netbox/dcim/migrations/0078_rack_type_to_slug.py rename to netbox/dcim/migrations/0079_3569_rack_fields.py index 2ebcd7168..137cd6fe5 100644 --- a/netbox/dcim/migrations/0078_rack_type_to_slug.py +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -8,6 +8,14 @@ RACK_TYPE_CHOICES = ( (1100, 'wall-cabinet'), ) +RACK_STATUS_CHOICES = ( + (0, 'reserved'), + (1, 'available'), + (2, 'planned'), + (3, 'active'), + (4, 'deprecated'), +) + def rack_type_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') @@ -15,14 +23,22 @@ def rack_type_to_slug(apps, schema_editor): 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) + + class Migration(migrations.Migration): atomic = False dependencies = [ - ('dcim', '0077_power_types'), + ('dcim', '0078_3569_site_fields'), ] operations = [ + + # Rack.type migrations.AlterField( model_name='rack', name='type', @@ -36,4 +52,14 @@ class Migration(migrations.Migration): 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 + ), ] diff --git a/netbox/dcim/migrations/0079_rack_status_to_slug.py b/netbox/dcim/migrations/0079_rack_status_to_slug.py deleted file mode 100644 index 76890d796..000000000 --- a/netbox/dcim/migrations/0079_rack_status_to_slug.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.db import migrations, models - -RACK_STATUS_CHOICES = ( - (0, 'reserved'), - (1, 'available'), - (2, 'planned'), - (3, 'active'), - (4, 'deprecated'), -) - - -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) - - -class Migration(migrations.Migration): - atomic = False - - dependencies = [ - ('dcim', '0078_rack_type_to_slug'), - ] - - operations = [ - migrations.AlterField( - model_name='rack', - name='status', - field=models.CharField(default='active', max_length=50), - ), - migrations.RunPython( - code=rack_status_to_slug - ), - ] diff --git a/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py b/netbox/dcim/migrations/0080_3569_devicetype_fields.py similarity index 95% rename from netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py rename to netbox/dcim/migrations/0080_3569_devicetype_fields.py index 2497f1702..afd4dd82b 100644 --- a/netbox/dcim/migrations/0081_devicetype_subdevicerole_to_slug.py +++ b/netbox/dcim/migrations/0080_3569_devicetype_fields.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('dcim', '0080_device_face_to_slug'), + ('dcim', '0079_3569_rack_fields'), ] operations = [ diff --git a/netbox/dcim/migrations/0080_device_face_to_slug.py b/netbox/dcim/migrations/0081_3569_device_fields.py similarity index 94% rename from netbox/dcim/migrations/0080_device_face_to_slug.py rename to netbox/dcim/migrations/0081_3569_device_fields.py index 4fe214770..fc1e9285a 100644 --- a/netbox/dcim/migrations/0080_device_face_to_slug.py +++ b/netbox/dcim/migrations/0081_3569_device_fields.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('dcim', '0079_rack_status_to_slug'), + ('dcim', '0080_3569_devicetype_fields'), ] operations = [ diff --git a/netbox/dcim/migrations/0082_interface_type_to_slug.py b/netbox/dcim/migrations/0082_3569_interface_fields.py similarity index 98% rename from netbox/dcim/migrations/0082_interface_type_to_slug.py rename to netbox/dcim/migrations/0082_3569_interface_fields.py index 50885d461..d325844fe 100644 --- a/netbox/dcim/migrations/0082_interface_type_to_slug.py +++ b/netbox/dcim/migrations/0082_3569_interface_fields.py @@ -91,7 +91,7 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('dcim', '0081_devicetype_subdevicerole_to_slug'), + ('dcim', '0081_3569_device_fields'), ] operations = [ From 3fa4ceadb0d79ebc88aacf69091d4fbdb0101016 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Nov 2019 22:39:15 -0500 Subject: [PATCH 17/47] Interface.mode to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 33 ++++++++++++++----- netbox/dcim/constants.py | 8 ----- netbox/dcim/forms.py | 8 ++--- .../migrations/0082_3569_interface_fields.py | 26 +++++++++++++++ netbox/dcim/models.py | 8 ++--- netbox/dcim/tests/test_api.py | 10 +++--- netbox/virtualization/api/serializers.py | 5 ++- netbox/virtualization/forms.py | 11 +++---- netbox/virtualization/tests/test_api.py | 11 +++---- 10 files changed, 76 insertions(+), 46 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7018a8fc9..bd61f2a9b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -469,7 +469,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() 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(), diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 12df4f701..c5c0c7707 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -111,9 +111,7 @@ class DeviceFaceChoices(ChoiceSet): # class ConsolePortTypeChoices(ChoiceSet): - """ - ConsolePort/ConsoleServerPort.type slugs - """ + TYPE_DE9 = 'de-9' TYPE_DB25 = 'db-25' TYPE_RJ45 = 'rj-45' @@ -152,6 +150,7 @@ class ConsolePortTypeChoices(ChoiceSet): # class PowerPortTypeChoices(ChoiceSet): + # TODO: Add more power port types # IEC 60320 TYPE_IEC_C6 = 'iec-60320-c6' @@ -239,6 +238,7 @@ class PowerPortTypeChoices(ChoiceSet): # class PowerOutletTypeChoices(ChoiceSet): + # TODO: Add more power outlet types # IEC 60320 TYPE_IEC_C5 = 'iec-60320-c5' @@ -326,9 +326,7 @@ class PowerOutletTypeChoices(ChoiceSet): # class InterfaceTypeChoices(ChoiceSet): - """ - Interface.type slugs - """ + # Virtual TYPE_VIRTUAL = 'virtual' TYPE_LAG = 'lag' @@ -623,14 +621,31 @@ class InterfaceTypeChoices(ChoiceSet): } +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, + } + + # # FrontPorts/RearPorts # class PortTypeChoices(ChoiceSet): - """ - FrontPort/RearPort.type slugs - """ + TYPE_8P8C = '8p8c' TYPE_110_PUNCH = '110-punch' TYPE_BNC = 'bnc' diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 4b199cf15..72fec872d 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -20,14 +20,6 @@ WIRELESS_IFACE_TYPES = [ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES -IFACE_MODE_ACCESS = 100 -IFACE_MODE_TAGGED = 200 -IFACE_MODE_TAGGED_ALL = 300 -IFACE_MODE_CHOICES = [ - [IFACE_MODE_ACCESS, 'Access'], - [IFACE_MODE_TAGGED, 'Tagged'], - [IFACE_MODE_TAGGED_ALL, 'Tagged All'], -] # Pass-through port types PORT_TYPE_8P8C = 1000 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 0a4444907..1ea8b2571 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -66,13 +66,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'] = [] @@ -2450,7 +2450,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(), ) @@ -2564,7 +2564,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() ) diff --git a/netbox/dcim/migrations/0082_3569_interface_fields.py b/netbox/dcim/migrations/0082_3569_interface_fields.py index d325844fe..b3606d32a 100644 --- a/netbox/dcim/migrations/0082_3569_interface_fields.py +++ b/netbox/dcim/migrations/0082_3569_interface_fields.py @@ -75,6 +75,13 @@ INTERFACE_TYPE_CHOICES = ( ) +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: @@ -87,6 +94,12 @@ def interface_type_to_slug(apps, schema_editor): 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 @@ -111,4 +124,17 @@ class Migration(migrations.Migration): migrations.RunPython( code=interface_type_to_slug ), + 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/models.py b/netbox/dcim/models.py index 4e67f61cc..6ca145d72 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2261,10 +2261,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', @@ -2373,7 +2373,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) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 730639704..17cbe9e6d 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -3,7 +3,7 @@ from netaddr import IPNetwork from rest_framework import status from circuits.models import Circuit, CircuitTermination, CircuitType, Provider -from dcim.choices import SubdeviceRoleChoices +from dcim.choices import InterfaceModeChoices, SubdeviceRoleChoices from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -2474,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], } @@ -2521,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], }, diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 4b519e5e2..98cc63226 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -3,8 +3,7 @@ from rest_framework import serializers from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer -from dcim.choices import InterfaceTypeChoices -from dcim.constants import 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 @@ -100,7 +99,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): virtual_machine = NestedVirtualMachineSerializer() type = ChoiceField(choices=InterfaceTypeChoices, default=InterfaceTypeChoices.TYPE_VIRTUAL, required=False) - 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(), diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 09848e0c7..5bfca6654 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -2,8 +2,7 @@ from django import forms from django.core.exceptions import ValidationError from taggit.forms import TagField -from dcim.choices import InterfaceTypeChoices -from dcim.constants import 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 @@ -718,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'] = [] @@ -755,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(), ) @@ -840,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() ) diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 64d9cf8b9..683a65a2b 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -2,8 +2,7 @@ from django.urls import reverse from netaddr import IPNetwork from rest_framework import status -from dcim.choices import InterfaceTypeChoices -from dcim.constants import 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 @@ -552,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], } @@ -599,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], }, From 5c95927a43bf2e22eeeb3472f966ca7f06d09792 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Nov 2019 22:54:01 -0500 Subject: [PATCH 18/47] Site.status to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 23 +++++++++++++++++++ netbox/dcim/constants.py | 10 -------- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 6 ++--- .../dcim/migrations/0078_3569_site_fields.py | 20 ++++++++++++++++ netbox/dcim/models.py | 15 ++++++++---- netbox/dcim/tests/test_api.py | 10 ++++---- 8 files changed, 64 insertions(+), 24 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index bd61f2a9b..5b9c26876 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) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index c5c0c7707..ac161daf8 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1,6 +1,29 @@ from utilities.choices import ChoiceSet +# +# Sites +# + +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 # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 72fec872d..71fbd6cd2 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -79,16 +79,6 @@ DEVICE_STATUS_CHOICES = [ [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', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 610a05bd9..4620ea17a 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -49,7 +49,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet): label='Search', ) status = django_filters.MultipleChoiceFilter( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, null_value=None ) region_id = TreeNodeMultipleChoiceFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1ea8b2571..15dc79ec5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -223,7 +223,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class SiteCSVForm(forms.ModelForm): status = CSVChoiceField( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, required=False, help_text='Operational status' ) @@ -262,7 +262,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() @@ -311,7 +311,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): label='Search' ) status = forms.MultipleChoiceField( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/dcim/migrations/0078_3569_site_fields.py b/netbox/dcim/migrations/0078_3569_site_fields.py index 0826f7f78..502ad6214 100644 --- a/netbox/dcim/migrations/0078_3569_site_fields.py +++ b/netbox/dcim/migrations/0078_3569_site_fields.py @@ -1,5 +1,17 @@ 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 @@ -9,4 +21,12 @@ class Migration(migrations.Migration): ] operations = [ + 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/models.py b/netbox/dcim/models.py index 6ca145d72..c8d9c8250 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) # diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 17cbe9e6d..466b40b37 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -3,7 +3,7 @@ from netaddr import IPNetwork from rest_framework import status from circuits.models import Circuit, CircuitTermination, CircuitType, Provider -from dcim.choices import InterfaceModeChoices, SubdeviceRoleChoices +from dcim.choices import * from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -181,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') @@ -201,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, }, ] From bcc34f609998f9a3aea28e46ccde375e98059250 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 19:23:43 -0500 Subject: [PATCH 19/47] Device.face to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 31 +++++++++++++++++++ netbox/dcim/constants.py | 18 ----------- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 6 ++-- .../migrations/0081_3569_device_fields.py | 29 +++++++++++++++++ netbox/dcim/models.py | 20 +++++++++--- netbox/dcim/tests/test_forms.py | 8 ++--- netbox/virtualization/constants.py | 8 ++--- netbox/virtualization/models.py | 2 +- 10 files changed, 89 insertions(+), 37 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5b9c26876..795040b17 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -325,7 +325,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): site = NestedSiteSerializer() rack = NestedRackSerializer(required=False, allow_null=True) face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) - status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) + status = ChoiceField(choices=DeviceStatusChoices, required=False) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index ac161daf8..2ba3845f6 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -129,6 +129,37 @@ class DeviceFaceChoices(ChoiceSet): } +class DeviceStatusChoices(ChoiceSet): + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_STAGED = 'staged' + STATUS_FAILED = 'failed' + STATUS_INVENTORY = 'inventory' + STATUS_DECOMMISSIONING = 'decommissioning' + + CHOICES = ( + (STATUS_OFFLINE, 'Offline'), + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_STAGED, 'Staged'), + (STATUS_FAILED, 'Failed'), + (STATUS_INVENTORY, 'Inventory'), + (STATUS_DECOMMISSIONING, 'Decommissioning'), + ) + + LEGACY_MAP = { + STATUS_OFFLINE: 0, + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_STAGED: 3, + STATUS_FAILED: 4, + STATUS_INVENTORY: 5, + STATUS_DECOMMISSIONING: 6, + } + + # # ConsolePorts # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 71fbd6cd2..a1b6744b7 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -61,24 +61,6 @@ PORT_TYPE_CHOICES = [ ] ] -# Device statuses -DEVICE_STATUS_OFFLINE = 0 -DEVICE_STATUS_ACTIVE = 1 -DEVICE_STATUS_PLANNED = 2 -DEVICE_STATUS_STAGED = 3 -DEVICE_STATUS_FAILED = 4 -DEVICE_STATUS_INVENTORY = 5 -DEVICE_STATUS_DECOMMISSIONING = 6 -DEVICE_STATUS_CHOICES = [ - [DEVICE_STATUS_ACTIVE, 'Active'], - [DEVICE_STATUS_OFFLINE, 'Offline'], - [DEVICE_STATUS_PLANNED, 'Planned'], - [DEVICE_STATUS_STAGED, 'Staged'], - [DEVICE_STATUS_FAILED, 'Failed'], - [DEVICE_STATUS_INVENTORY, 'Inventory'], - [DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'], -] - # Bootstrap CSS classes for device/rack statuses STATUS_CLASSES = { 0: 'warning', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 4620ea17a..e3c6f64ac 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -511,7 +511,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter label='Device model (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, null_value=None ) is_full_depth = django_filters.BooleanFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 15dc79ec5..d6c852c2d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1665,7 +1665,7 @@ class BaseDeviceCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, help_text='Operational status' ) @@ -1833,7 +1833,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ) ) status = forms.ChoiceField( - choices=add_blank_choice(DEVICE_STATUS_CHOICES), + choices=add_blank_choice(DeviceStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -1944,7 +1944,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt ) ) status = forms.MultipleChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/dcim/migrations/0081_3569_device_fields.py b/netbox/dcim/migrations/0081_3569_device_fields.py index fc1e9285a..f1f0bdb2b 100644 --- a/netbox/dcim/migrations/0081_3569_device_fields.py +++ b/netbox/dcim/migrations/0081_3569_device_fields.py @@ -5,6 +5,16 @@ DEVICE_FACE_CHOICES = ( (1, 'rear'), ) +DEVICE_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (3, 'staged'), + (4, 'failed'), + (5, 'inventory'), + (6, 'decommissioning'), +) + def device_face_to_slug(apps, schema_editor): Device = apps.get_model('dcim', 'Device') @@ -12,6 +22,12 @@ def device_face_to_slug(apps, schema_editor): Device.objects.filter(face=str(id)).update(face=slug) +def device_status_to_slug(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for id, slug in DEVICE_STATUS_CHOICES: + Device.objects.filter(status=str(id)).update(status=slug) + + class Migration(migrations.Migration): atomic = False @@ -20,6 +36,8 @@ class Migration(migrations.Migration): ] operations = [ + + # Device.face migrations.AlterField( model_name='device', name='face', @@ -33,4 +51,15 @@ class Migration(migrations.Migration): name='face', field=models.CharField(blank=True, max_length=50), ), + + # Device.status + migrations.AlterField( + model_name='device', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=device_status_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c8d9c8250..f9707a90a 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1551,10 +1551,10 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): choices=DeviceFaceChoices, verbose_name='Rack face' ) - status = models.PositiveSmallIntegerField( - choices=DEVICE_STATUS_CHOICES, - default=DEVICE_STATUS_ACTIVE, - verbose_name='Status' + status = models.CharField( + max_length=50, + choices=DeviceStatusChoices, + default=DeviceStatusChoices.STATUS_ACTIVE ) primary_ip4 = models.OneToOneField( to='ipam.IPAddress', @@ -1616,6 +1616,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] + STATUS_CLASS_MAP = { + DeviceStatusChoices.STATUS_OFFLINE: 'warning', + DeviceStatusChoices.STATUS_ACTIVE: 'success', + DeviceStatusChoices.STATUS_PLANNED: 'info', + DeviceStatusChoices.STATUS_STAGED: 'primary', + DeviceStatusChoices.STATUS_FAILED: 'danger', + DeviceStatusChoices.STATUS_INVENTORY: 'default', + DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning', + } + class Meta: ordering = ['name'] unique_together = [ @@ -1869,7 +1879,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): return Device.objects.filter(parent_bay__device=self.pk) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) # diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 8ac9aa84e..d7a946568 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -24,7 +24,7 @@ class DeviceTestCase(TestCase): 'face': DeviceFaceChoices.FACE_FRONT, 'position': 41, 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid(), test.fields['position'].choices) self.assertTrue(test.save()) @@ -41,7 +41,7 @@ class DeviceTestCase(TestCase): 'face': DeviceFaceChoices.FACE_FRONT, 'position': 1, 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertFalse(test.is_valid()) @@ -57,7 +57,7 @@ class DeviceTestCase(TestCase): 'face': '', 'position': None, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid()) self.assertTrue(test.save()) @@ -74,7 +74,7 @@ class DeviceTestCase(TestCase): 'face': DeviceFaceChoices.FACE_REAR, 'position': None, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid()) self.assertTrue(test.save()) diff --git a/netbox/virtualization/constants.py b/netbox/virtualization/constants.py index 37e9efea2..3eeddd066 100644 --- a/netbox/virtualization/constants.py +++ b/netbox/virtualization/constants.py @@ -1,10 +1,10 @@ -from dcim.constants import DEVICE_STATUS_ACTIVE, DEVICE_STATUS_OFFLINE, DEVICE_STATUS_STAGED +from dcim.choices import DeviceStatusChoices # VirtualMachine statuses (replicated from Device statuses) VM_STATUS_CHOICES = [ - [DEVICE_STATUS_ACTIVE, 'Active'], - [DEVICE_STATUS_OFFLINE, 'Offline'], - [DEVICE_STATUS_STAGED, 'Staged'], + [1, 'Active'], + [0, 'Offline'], + [3, 'Staged'], ] # Bootstrap CSS classes for VirtualMachine statuses diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index c47f516cf..790c9c190 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -195,7 +195,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): ) status = models.PositiveSmallIntegerField( choices=VM_STATUS_CHOICES, - default=DEVICE_STATUS_ACTIVE, + default=1, # TODO: Replace with ChoiceSet value verbose_name='Status' ) role = models.ForeignKey( From dead5b42be049d957a9f00f9f6193baf98ceff2c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 19:39:25 -0500 Subject: [PATCH 20/47] Front/RearPort.type to slug (#3569) --- netbox/dcim/api/serializers.py | 8 +- netbox/dcim/constants.py | 41 --------- netbox/dcim/forms.py | 22 ++--- .../dcim/migrations/0082_3569_port_fields.py | 85 +++++++++++++++++++ netbox/dcim/models.py | 20 +++-- netbox/dcim/tests/test_api.py | 24 +++--- netbox/dcim/tests/test_models.py | 16 ++-- 7 files changed, 127 insertions(+), 89 deletions(-) create mode 100644 netbox/dcim/migrations/0082_3569_port_fields.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 795040b17..04752641a 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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: @@ -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) diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index a1b6744b7..5892df352 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -20,47 +20,6 @@ WIRELESS_IFACE_TYPES = [ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES - -# 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'], - ] - ] -] - # Bootstrap CSS classes for device/rack statuses STATUS_CLASSES = { 0: 'warning', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index d6c852c2d..51e454a16 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1133,7 +1133,7 @@ class FrontPortTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2() ) rear_port_set = forms.MultipleChoiceField( @@ -1203,7 +1203,7 @@ class RearPortTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) positions = forms.IntegerField( @@ -1328,11 +1328,6 @@ 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 PortTypeChoices.slug_to_id(slug) - class RearPortTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( @@ -1345,11 +1340,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 PortTypeChoices.slug_to_id(slug) - class DeviceBayTemplateImportForm(ComponentTemplateImportForm): @@ -2686,7 +2676,7 @@ class FrontPortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) rear_port_set = forms.MultipleChoiceField( @@ -2746,7 +2736,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() ) @@ -2800,7 +2790,7 @@ class RearPortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) positions = forms.IntegerField( @@ -2820,7 +2810,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() ) 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..9cabc3bd5 --- /dev/null +++ b/netbox/dcim/migrations/0082_3569_port_fields.py @@ -0,0 +1,85 @@ +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 = [ + migrations.AlterField( + model_name='frontporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontporttemplate_type_to_slug + ), + migrations.AlterField( + model_name='rearporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearporttemplate_type_to_slug + ), + migrations.AlterField( + model_name='frontport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontport_type_to_slug + ), + 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/models.py b/netbox/dcim/models.py index f9707a90a..926377f9f 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1252,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', @@ -1319,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, @@ -2475,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', @@ -2542,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, diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 466b40b37..ebeba944b 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -3034,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') @@ -3162,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') @@ -3273,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') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index fae6a589d..3388b5c84 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -200,7 +200,7 @@ class DeviceTestCase(TestCase): 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() @@ -264,14 +264,14 @@ class DeviceTestCase(TestCase): 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 ) @@ -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): From 79a40e22c9a3e7526230aaf43a08e2d04f703366 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 19:57:13 -0500 Subject: [PATCH 21/47] Cable.type to slug (#3569) --- netbox/dcim/choices.py | 79 +++++++++++++++++++ netbox/dcim/constants.py | 50 ------------ netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 6 +- .../dcim/migrations/0083_3569_cable_fields.py | 54 +++++++++++++ netbox/dcim/models.py | 8 +- netbox/dcim/tests/test_views.py | 8 +- 7 files changed, 145 insertions(+), 62 deletions(-) create mode 100644 netbox/dcim/migrations/0083_3569_cable_fields.py diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2ba3845f6..b86ef9d6a 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -755,3 +755,82 @@ class PortTypeChoices(ChoiceSet): 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, + } diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 5892df352..7aff330e5 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -45,56 +45,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'), diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e3c6f64ac..4e8e21f6d 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -939,7 +939,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/forms.py b/netbox/dcim/forms.py index 51e454a16..8cfeb2736 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3144,7 +3144,7 @@ class CableCSVForm(forms.ModelForm): help_text='Connection status' ) type = CSVChoiceField( - choices=CABLE_TYPE_CHOICES, + choices=CableTypeChoices, required=False, help_text='Cable type' ) @@ -3229,7 +3229,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() @@ -3303,7 +3303,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() ) 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..432935ecf --- /dev/null +++ b/netbox/dcim/migrations/0083_3569_cable_fields.py @@ -0,0 +1,54 @@ +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'), +) + + +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) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0082_3569_port_fields'), + ] + + operations = [ + 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), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 926377f9f..3e1de04d7 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2809,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, diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 6bda7c6c5..9315b201d 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -527,15 +527,15 @@ class CableTestCase(TestCase): 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))) From 4846557d61befa271d914e1bc7e3a02264f8b03b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 20:40:29 -0500 Subject: [PATCH 22/47] Cable.length_unit to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 22 +++++++++++++ netbox/dcim/constants.py | 6 ---- netbox/dcim/forms.py | 4 +-- .../dcim/migrations/0083_3569_cable_fields.py | 31 +++++++++++++++++++ netbox/dcim/models.py | 10 +++--- 6 files changed, 61 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 04752641a..283382515 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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 diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index b86ef9d6a..d9e02ab4f 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -834,3 +834,25 @@ class CableTypeChoices(ChoiceSet): 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, + } diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 7aff330e5..d2b715415 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -72,12 +72,6 @@ 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'), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 8cfeb2736..c606c4797 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3149,7 +3149,7 @@ class CableCSVForm(forms.ModelForm): help_text='Cable type' ) length_unit = CSVChoiceField( - choices=CABLE_LENGTH_UNIT_CHOICES, + choices=CableLengthUnitChoices, required=False, help_text='Length unit' ) @@ -3254,7 +3254,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() diff --git a/netbox/dcim/migrations/0083_3569_cable_fields.py b/netbox/dcim/migrations/0083_3569_cable_fields.py index 432935ecf..d6f013b37 100644 --- a/netbox/dcim/migrations/0083_3569_cable_fields.py +++ b/netbox/dcim/migrations/0083_3569_cable_fields.py @@ -23,6 +23,13 @@ CABLE_TYPE_CHOICES = ( (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') @@ -30,6 +37,12 @@ def cable_type_to_slug(apps, schema_editor): 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 @@ -38,6 +51,8 @@ class Migration(migrations.Migration): ] operations = [ + + # Cable.type migrations.AlterField( model_name='cable', name='type', @@ -51,4 +66,20 @@ class Migration(migrations.Migration): 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/models.py b/netbox/dcim/models.py index 3e1de04d7..52b55373d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2829,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( @@ -2960,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): From 9872a46583fedbb0be15480b68ae1acc01869d01 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 20:54:24 -0500 Subject: [PATCH 23/47] Rack.outer_unit to slug (#3569) --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 16 ++++++++++++ netbox/dcim/constants.py | 10 ------- netbox/dcim/forms.py | 4 +-- .../dcim/migrations/0079_3569_rack_fields.py | 26 +++++++++++++++++++ netbox/dcim/models.py | 10 +++---- netbox/utilities/utils.py | 18 ++++++++----- 7 files changed, 62 insertions(+), 24 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 283382515..13781a696 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -119,7 +119,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): role = NestedRackRoleSerializer(required=False, allow_null=True) type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True) width = ChoiceField(choices=RackWidthChoices, required=False) - outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, 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) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index d9e02ab4f..2ffb30b38 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -89,6 +89,22 @@ class RackStatusChoices(ChoiceSet): } +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 # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index d2b715415..4aca9a2a1 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -67,16 +67,6 @@ COMPATIBLE_TERMINATION_TYPES = { 'circuittermination': ['interface', 'frontport', 'rearport'], } -LENGTH_UNIT_METER = 1200 -LENGTH_UNIT_CENTIMETER = 1100 -LENGTH_UNIT_MILLIMETER = 1000 -LENGTH_UNIT_FOOT = 2100 -LENGTH_UNIT_INCH = 2000 -RACK_DIMENSION_UNIT_CHOICES = ( - (LENGTH_UNIT_MILLIMETER, 'Millimeters'), - (LENGTH_UNIT_INCH, 'Inches'), -) - # Power feeds POWERFEED_TYPE_PRIMARY = 1 POWERFEED_TYPE_REDUNDANT = 2 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c606c4797..bf34c7971 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -496,7 +496,7 @@ class RackCSVForm(forms.ModelForm): 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' ) @@ -617,7 +617,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() ) diff --git a/netbox/dcim/migrations/0079_3569_rack_fields.py b/netbox/dcim/migrations/0079_3569_rack_fields.py index 137cd6fe5..a33b83eb7 100644 --- a/netbox/dcim/migrations/0079_3569_rack_fields.py +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -16,6 +16,11 @@ RACK_STATUS_CHOICES = ( (4, 'deprecated'), ) +RACK_DIMENSION_CHOICES = ( + (1000, 'mm'), + (2000, 'in'), +) + def rack_type_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') @@ -29,6 +34,12 @@ def rack_status_to_slug(apps, schema_editor): 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 @@ -62,4 +73,19 @@ class Migration(migrations.Migration): 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/models.py b/netbox/dcim/models.py index 52b55373d..6d8748313 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -535,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 @@ -584,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 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)) From 62494f295edb2fbb0accffc3561ffae3b7b7f963 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 21:03:11 -0500 Subject: [PATCH 24/47] PowerFeed.type to slug (#3569) --- netbox/dcim/api/serializers.py | 4 +-- netbox/dcim/choices.py | 20 +++++++++++ netbox/dcim/constants.py | 6 ---- netbox/dcim/forms.py | 6 ++-- .../migrations/0084_3569_powerfeed_fields.py | 35 +++++++++++++++++++ netbox/dcim/models.py | 6 ++-- netbox/dcim/tests/test_api.py | 20 +++++------ 7 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 netbox/dcim/migrations/0084_3569_powerfeed_fields.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 13781a696..7883ca169 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -691,8 +691,8 @@ 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, diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2ffb30b38..7b7b18b4d 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -872,3 +872,23 @@ class CableLengthUnitChoices(ChoiceSet): UNIT_FOOT: 2100, UNIT_INCH: 2000, } + + +# +# PowerFeeds +# + +class PowerFeedTypeChoices(ChoiceSet): + + TYPE_PRIMARY = 'primary' + TYPE_REDUNDANT = 'redundant' + + CHOICES = ( + (TYPE_PRIMARY, 'Primary'), + (TYPE_REDUNDANT, 'Redundant'), + ) + + LEGACY_MAP = { + TYPE_PRIMARY: 1, + TYPE_REDUNDANT: 2, + } diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 4aca9a2a1..8a574b185 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -68,12 +68,6 @@ COMPATIBLE_TERMINATION_TYPES = { } # 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 = ( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index bf34c7971..32d60ace7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3860,7 +3860,7 @@ class PowerFeedCSVForm(forms.ModelForm): help_text='Operational status' ) type = CSVChoiceField( - choices=POWERFEED_TYPE_CHOICES, + choices=PowerFeedTypeChoices, required=False, help_text='Primary or redundant' ) @@ -3936,7 +3936,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd widget=StaticSelect2() ) type = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + choices=add_blank_choice(PowerFeedTypeChoices), required=False, initial='', widget=StaticSelect2() @@ -4014,7 +4014,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): widget=StaticSelect2Multiple() ) type = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + choices=add_blank_choice(PowerFeedTypeChoices), required=False, widget=StaticSelect2() ) 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..d40d367e6 --- /dev/null +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -0,0 +1,35 @@ +from django.db import migrations, models + + +POWERFEED_TYPE_CHOICES = ( + (1, 'primary'), + (2, 'redundant'), +) + + +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) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0083_3569_cable_fields'), + ] + + operations = [ + + # Cable.type + migrations.AlterField( + model_name='powerfeed', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=powerfeed_type_to_slug + ), + + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6d8748313..c1b951496 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -3111,9 +3111,9 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): choices=POWERFEED_STATUS_CHOICES, default=POWERFEED_STATUS_ACTIVE ) - type = models.PositiveSmallIntegerField( - choices=POWERFEED_TYPE_CHOICES, - default=POWERFEED_TYPE_PRIMARY + type = models.CharField( + choices=PowerFeedTypeChoices, + default=PowerFeedTypeChoices.TYPE_PRIMARY ) supply = models.PositiveSmallIntegerField( choices=POWERFEED_SUPPLY_CHOICES, diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index ebeba944b..c396407ac 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -3679,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): @@ -3727,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') @@ -3747,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, }, ] @@ -3770,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}) From bb8b012397be4383f95ee565041a4c32ec718e39 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 21:08:34 -0500 Subject: [PATCH 25/47] PowerFeed.supply to slug (#3569) --- netbox/dcim/api/serializers.py | 4 ++-- netbox/dcim/choices.py | 16 +++++++++++++ netbox/dcim/constants.py | 6 ----- netbox/dcim/forms.py | 6 ++--- .../migrations/0084_3569_powerfeed_fields.py | 23 ++++++++++++++++++- netbox/dcim/models.py | 8 ++++--- 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7883ca169..1f7603241 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -699,8 +699,8 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): default=POWERFEED_STATUS_ACTIVE ) supply = ChoiceField( - choices=POWERFEED_SUPPLY_CHOICES, - default=POWERFEED_SUPPLY_AC + choices=PowerFeedSupplyChoices, + default=PowerFeedSupplyChoices.SUPPLY_AC ) phase = ChoiceField( choices=POWERFEED_PHASE_CHOICES, diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 7b7b18b4d..5d44f1cf4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -892,3 +892,19 @@ class PowerFeedTypeChoices(ChoiceSet): TYPE_PRIMARY: 1, TYPE_REDUNDANT: 2, } + + +class PowerFeedSupplyChoices(ChoiceSet): + + SUPPLY_AC = 'ac' + SUPPLY_DC = 'dc' + + CHOICES = ( + (SUPPLY_AC, 'Primary'), + (SUPPLY_DC, 'Redundant'), + ) + + LEGACY_MAP = { + SUPPLY_AC: 1, + SUPPLY_DC: 2, + } diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 8a574b185..13335de3f 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -68,12 +68,6 @@ COMPATIBLE_TERMINATION_TYPES = { } # Power feeds -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 = ( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 32d60ace7..f767d0903 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3865,7 +3865,7 @@ class PowerFeedCSVForm(forms.ModelForm): help_text='Primary or redundant' ) supply = CSVChoiceField( - choices=POWERFEED_SUPPLY_CHOICES, + choices=PowerFeedSupplyChoices, required=False, help_text='AC/DC' ) @@ -3942,7 +3942,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd widget=StaticSelect2() ) supply = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + choices=add_blank_choice(PowerFeedSupplyChoices), required=False, initial='', widget=StaticSelect2() @@ -4019,7 +4019,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): widget=StaticSelect2() ) supply = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + choices=add_blank_choice(PowerFeedSupplyChoices), required=False, widget=StaticSelect2() ) diff --git a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py index d40d367e6..5004258bd 100644 --- a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -6,6 +6,11 @@ POWERFEED_TYPE_CHOICES = ( (2, 'redundant'), ) +POWERFEED_SUPPLY_CHOICES = ( + (1, 'ac'), + (2, 'dc'), +) + def powerfeed_type_to_slug(apps, schema_editor): PowerFeed = apps.get_model('dcim', 'PowerFeed') @@ -13,6 +18,12 @@ def powerfeed_type_to_slug(apps, schema_editor): 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) + + class Migration(migrations.Migration): atomic = False @@ -22,7 +33,7 @@ class Migration(migrations.Migration): operations = [ - # Cable.type + # PowerFeed.type migrations.AlterField( model_name='powerfeed', name='type', @@ -32,4 +43,14 @@ class Migration(migrations.Migration): code=powerfeed_type_to_slug ), + # PowerFeed.supply + migrations.AlterField( + model_name='powerfeed', + name='supply', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=powerfeed_supply_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c1b951496..3b9ee069e 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -3112,12 +3112,14 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): default=POWERFEED_STATUS_ACTIVE ) 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, From 2dc07ca30ded68746b1892b8995e73b6b0f06e9f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 21:14:04 -0500 Subject: [PATCH 26/47] PowerFeed.phase to slug (#3569) --- netbox/dcim/api/serializers.py | 4 ++-- netbox/dcim/choices.py | 20 ++++++++++++++++-- netbox/dcim/constants.py | 6 ------ netbox/dcim/forms.py | 6 +++--- .../migrations/0084_3569_powerfeed_fields.py | 21 +++++++++++++++++++ netbox/dcim/models.py | 11 +++++----- 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1f7603241..d03d3f5a9 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -703,8 +703,8 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): 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 5d44f1cf4..cd4124518 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -900,11 +900,27 @@ class PowerFeedSupplyChoices(ChoiceSet): SUPPLY_DC = 'dc' CHOICES = ( - (SUPPLY_AC, 'Primary'), - (SUPPLY_DC, 'Redundant'), + (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 13335de3f..79ad5229d 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -68,12 +68,6 @@ COMPATIBLE_TERMINATION_TYPES = { } # Power feeds -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 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f767d0903..fb9467397 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3870,7 +3870,7 @@ class PowerFeedCSVForm(forms.ModelForm): help_text='AC/DC' ) phase = CSVChoiceField( - choices=POWERFEED_PHASE_CHOICES, + choices=PowerFeedPhaseChoices, required=False, help_text='Single or three-phase' ) @@ -3948,7 +3948,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd widget=StaticSelect2() ) phase = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_PHASE_CHOICES), + choices=add_blank_choice(PowerFeedPhaseChoices), required=False, initial='', widget=StaticSelect2() @@ -4024,7 +4024,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): 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/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py index 5004258bd..5dafa4f3a 100644 --- a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -11,6 +11,11 @@ POWERFEED_SUPPLY_CHOICES = ( (2, 'dc'), ) +POWERFEED_PHASE_CHOICES = ( + (1, 'single-phase'), + (3, 'three-phase'), +) + def powerfeed_type_to_slug(apps, schema_editor): PowerFeed = apps.get_model('dcim', 'PowerFeed') @@ -24,6 +29,12 @@ def powerfeed_supply_to_slug(apps, schema_editor): 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 @@ -53,4 +64,14 @@ class Migration(migrations.Migration): code=powerfeed_supply_to_slug ), + # PowerFeed.phase + migrations.AlterField( + model_name='powerfeed', + name='phase', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=powerfeed_phase_to_slug + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 3b9ee069e..1510c7bea 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2108,7 +2108,7 @@ class PowerPort(CableTermination, ComponentModel): } # Calculate per-leg aggregates for three-phase feeds - if self._connected_powerfeed and self._connected_powerfeed.phase == POWERFEED_PHASE_3PHASE: + if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE: for leg, leg_name in POWERFEED_LEG_CHOICES: 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( @@ -3121,9 +3121,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): 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)], @@ -3197,7 +3198,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) From 8c7f6c62b0379402945c1eadaf5aed20caf50129 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 25 Nov 2019 21:21:35 -0500 Subject: [PATCH 27/47] PowerFeed.status to slug (#3569) --- netbox/dcim/api/serializers.py | 4 ++-- netbox/dcim/choices.py | 22 ++++++++++++++++++ netbox/dcim/constants.py | 21 ----------------- netbox/dcim/forms.py | 6 ++--- .../migrations/0084_3569_powerfeed_fields.py | 23 +++++++++++++++++++ netbox/dcim/models.py | 23 +++++++++++++++---- 6 files changed, 68 insertions(+), 31 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d03d3f5a9..eb640914e 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -695,8 +695,8 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): default=PowerFeedTypeChoices.TYPE_PRIMARY ) status = ChoiceField( - choices=POWERFEED_STATUS_CHOICES, - default=POWERFEED_STATUS_ACTIVE + choices=PowerFeedStatusChoices, + default=PowerFeedStatusChoices.STATUS_ACTIVE ) supply = ChoiceField( choices=PowerFeedSupplyChoices, diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index cd4124518..4ac4ecde0 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -878,6 +878,28 @@ class CableLengthUnitChoices(ChoiceSet): # 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' diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 79ad5229d..fb119286a 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -20,17 +20,6 @@ WIRELESS_IFACE_TYPES = [ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES -# 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 @@ -68,16 +57,6 @@ COMPATIBLE_TERMINATION_TYPES = { } # Power feeds -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 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index fb9467397..6350b5045 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3855,7 +3855,7 @@ class PowerFeedCSVForm(forms.ModelForm): help_text="Rack name (optional)" ) status = CSVChoiceField( - choices=POWERFEED_STATUS_CHOICES, + choices=PowerFeedStatusChoices, required=False, help_text='Operational status' ) @@ -3930,7 +3930,7 @@ 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() @@ -4009,7 +4009,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) status = forms.MultipleChoiceField( - choices=POWERFEED_STATUS_CHOICES, + choices=PowerFeedStatusChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py index 5dafa4f3a..6a3ba1ce5 100644 --- a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -1,6 +1,13 @@ from django.db import migrations, models +POWERFEED_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (4, 'failed'), +) + POWERFEED_TYPE_CHOICES = ( (1, 'primary'), (2, 'redundant'), @@ -17,6 +24,12 @@ POWERFEED_PHASE_CHOICES = ( ) +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: @@ -44,6 +57,16 @@ class Migration(migrations.Migration): operations = [ + # PowerFeed.status + migrations.AlterField( + model_name='powerfeed', + name='status', + field=models.CharField(blank=True, max_length=50), + ), + migrations.RunPython( + code=powerfeed_status_to_slug + ), + # PowerFeed.type migrations.AlterField( model_name='powerfeed', diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1510c7bea..b38adb479 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -3107,9 +3107,10 @@ 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.CharField( max_length=50, @@ -3159,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'] @@ -3206,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) From 21fe5902a80f4435fa5925e87c27bec345c34348 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Nov 2019 21:29:58 -0500 Subject: [PATCH 28/47] PowerOutlet.feed_leg to slug (#3569) --- netbox/dcim/api/serializers.py | 4 +- netbox/dcim/choices.py | 19 ++++++ netbox/dcim/constants.py | 10 --- netbox/dcim/forms.py | 6 +- .../0085_3569_poweroutlet_fields.py | 62 +++++++++++++++++++ netbox/dcim/models.py | 14 ++--- netbox/dcim/tests/test_models.py | 4 +- netbox/dcim/tests/test_views.py | 8 +-- 8 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 netbox/dcim/migrations/0085_3569_poweroutlet_fields.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index eb640914e..3c4170373 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -245,7 +245,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer): required=False ) feed_leg = ChoiceField( - choices=POWERFEED_LEG_CHOICES, + choices=PowerOutletFeedLegChoices, required=False, allow_null=True ) @@ -429,7 +429,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): required=False ) feed_leg = ChoiceField( - choices=POWERFEED_LEG_CHOICES, + choices=PowerOutletFeedLegChoices, required=False, allow_null=True ) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 4ac4ecde0..ccffa4379 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -391,6 +391,25 @@ class PowerOutletTypeChoices(ChoiceSet): ) +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, + } + + # # Interfaces # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index fb119286a..8dacd68f5 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -55,13 +55,3 @@ COMPATIBLE_TERMINATION_TYPES = { 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], 'circuittermination': ['interface', 'frontport', 'rearport'], } - -# Power feeds -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/forms.py b/netbox/dcim/forms.py index 6350b5045..c177589e2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1043,7 +1043,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() ) @@ -2241,7 +2241,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( @@ -2270,7 +2270,7 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): required=False ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False, ) power_port = forms.ModelChoiceField( 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 b38adb479..6f46c0c96 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1164,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)" ) @@ -2109,7 +2109,7 @@ class PowerPort(CableTermination, ComponentModel): # Calculate per-leg aggregates for three-phase feeds if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE: - for leg, leg_name in POWERFEED_LEG_CHOICES: + 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'), @@ -2161,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( diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 3388b5c84..2c3507758 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -187,7 +187,7 @@ 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( @@ -251,7 +251,7 @@ 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( diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 9315b201d..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 @@ -343,7 +343,7 @@ device-bays: self.assertEqual(po1.name, 'Power Outlet 1') 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() From 929c0648d083ef06cc347fac3a28db12fbe13268 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Nov 2019 21:46:53 -0500 Subject: [PATCH 29/47] Prefix.status to slug (#3569) --- netbox/ipam/api/serializers.py | 3 +- netbox/ipam/choices.py | 27 +++++++++++++++ netbox/ipam/constants.py | 12 ------- netbox/ipam/filters.py | 3 +- netbox/ipam/fixtures/ipam.json | 4 +-- netbox/ipam/forms.py | 7 ++-- .../migrations/0028_3569_prefix_fields.py | 34 +++++++++++++++++++ netbox/ipam/models.py | 23 +++++++++---- netbox/ipam/views.py | 7 ++-- 9 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 netbox/ipam/choices.py create mode 100644 netbox/ipam/migrations/0028_3569_prefix_fields.py diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index fc0c390cf..fa9269584 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -8,6 +8,7 @@ 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.choices import * from ipam.constants import * from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.nested_serializers import NestedTenantSerializer @@ -140,7 +141,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) diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py new file mode 100644 index 000000000..fc9874bb4 --- /dev/null +++ b/netbox/ipam/choices.py @@ -0,0 +1,27 @@ +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, + } diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index e2fd1ca01..93521e77d 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -4,18 +4,6 @@ AF_CHOICES = ( (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 diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index c57006b27..d75d312cd 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -9,6 +9,7 @@ from extras.filters import CustomFieldFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine +from .choices import * from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -178,7 +179,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet): label='Role (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=PREFIX_STATUS_CHOICES, + choices=PrefixStatusChoices, null_value=None ) tag = TagFilter() diff --git a/netbox/ipam/fixtures/ipam.json b/netbox/ipam/fixtures/ipam.json index 10e22b2d7..9d8da496c 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": "" } diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 002d2a72a..0c7108f41 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -13,6 +13,7 @@ from utilities.forms import ( StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES ) from virtualization.models import VirtualMachine +from .choices import * from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -374,7 +375,7 @@ class PrefixCSVForm(forms.ModelForm): required=False ) status = CSVChoiceField( - choices=PREFIX_STATUS_CHOICES, + choices=PrefixStatusChoices, help_text='Operational status' ) role = forms.ModelChoiceField( @@ -459,7 +460,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 +528,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) ) ) status = forms.MultipleChoiceField( - choices=PREFIX_STATUS_CHOICES, + choices=PrefixStatusChoices, required=False, widget=StaticSelect2Multiple() ) 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..1aaa9c22b --- /dev/null +++ b/netbox/ipam/migrations/0028_3569_prefix_fields.py @@ -0,0 +1,34 @@ +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 = [ + 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/models.py b/netbox/ipam/models.py index 8f9b64b59..bb922c287 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -14,6 +14,7 @@ from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from virtualization.models import VirtualMachine +from .choices import * from .constants import * from .fields import IPNetworkField, IPAddressField from .querysets import PrefixQuerySet @@ -297,9 +298,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 +335,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 +413,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 +423,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 +433,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 +499,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) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 2cc1a0ea8..ce1d9bca7 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -14,6 +14,7 @@ from utilities.views import ( ) from virtualization.models import VirtualMachine from . import filters, forms, tables +from .choices import PrefixStatusChoices from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -217,13 +218,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. From ba8f324b12a7d7e8b5452cc309c86a07e2b6a74e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Nov 2019 21:54:01 -0500 Subject: [PATCH 30/47] IPAddress.status to slug (#3569) --- netbox/ipam/api/serializers.py | 2 +- netbox/ipam/choices.py | 26 +++++++++++++ netbox/ipam/constants.py | 12 ------ netbox/ipam/filters.py | 2 +- netbox/ipam/forms.py | 6 +-- .../migrations/0029_3569_ipaddress_fields.py | 37 +++++++++++++++++++ netbox/ipam/models.py | 17 ++++++--- 7 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 netbox/ipam/migrations/0029_3569_ipaddress_fields.py diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index fa9269584..4c49dbf57 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -201,7 +201,7 @@ 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) + status = ChoiceField(choices=IPAddressStatusChoices, required=False) role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True) interface = IPAddressInterfaceSerializer(required=False, allow_null=True) nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index fc9874bb4..2cdda3731 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -25,3 +25,29 @@ class PrefixStatusChoices(ChoiceSet): 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, + } diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index 93521e77d..6a95e247c 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -4,18 +4,6 @@ AF_CHOICES = ( (6, 'IPv6'), ) -# 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 diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index d75d312cd..c4be858fa 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -311,7 +311,7 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): label='Interface (ID)', ) status = django_filters.MultipleChoiceFilter( - choices=IPADDRESS_STATUS_CHOICES, + choices=IPAddressStatusChoices, null_value=None ) role = django_filters.MultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 0c7108f41..e2f5140d2 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -765,7 +765,7 @@ class IPAddressCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=IPADDRESS_STATUS_CHOICES, + choices=IPAddressStatusChoices, help_text='Operational status' ) role = CSVChoiceField( @@ -894,7 +894,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd ) ) status = forms.ChoiceField( - choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), + choices=add_blank_choice(IPAddressStatusChoices), required=False, widget=StaticSelect2() ) @@ -973,7 +973,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo ) ) status = forms.MultipleChoiceField( - choices=IPADDRESS_STATUS_CHOICES, + choices=IPAddressStatusChoices, required=False, widget=StaticSelect2Multiple() ) 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..ae5ede5e9 --- /dev/null +++ b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py @@ -0,0 +1,37 @@ +from django.db import migrations, models + + +IPADDRESS_STATUS_CHOICES = ( + (0, 'container'), + (1, 'active'), + (2, 'reserved'), + (3, 'deprecated'), +) + + +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) + + +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 + ), + + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index bb922c287..631805cb1 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -559,10 +559,10 @@ 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( @@ -613,6 +613,13 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): 'dns_name', 'description', ] + STATUS_CLASS_MAP = { + 'active': 'primary', + 'reserved': 'info', + 'deprecated': 'danger', + 'dhcp': 'success', + } + class Meta: ordering = ['family', 'address'] verbose_name = 'IP address' @@ -746,7 +753,7 @@ 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] From 14a7a33cc258d487756a6c4a33df36f06839e49b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Nov 2019 22:09:16 -0500 Subject: [PATCH 31/47] IPAddress.role to slug (#3569) --- netbox/ipam/api/serializers.py | 2 +- netbox/ipam/choices.py | 34 +++++++++++++++ netbox/ipam/constants.py | 41 +------------------ netbox/ipam/filters.py | 2 +- netbox/ipam/forms.py | 6 +-- .../migrations/0029_3569_ipaddress_fields.py | 32 +++++++++++++++ netbox/ipam/models.py | 31 +++++++++++--- netbox/ipam/tests/test_models.py | 6 +-- netbox/ipam/views.py | 6 +-- 9 files changed, 104 insertions(+), 56 deletions(-) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 4c49dbf57..8792063aa 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -202,7 +202,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer): vrf = NestedVRFSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceField(choices=IPAddressStatusChoices, required=False) - role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True) + 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) diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index 2cdda3731..daf8aebec 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -51,3 +51,37 @@ class IPAddressStatusChoices(ChoiceSet): 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, + } diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index 6a95e247c..59e25489c 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -4,36 +4,6 @@ AF_CHOICES = ( (6, 'IPv6'), ) -# 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 @@ -53,16 +23,7 @@ STATUS_CHOICE_CLASSES = { 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 diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index c4be858fa..26e5297bd 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -315,7 +315,7 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): null_value=None ) role = django_filters.MultipleChoiceFilter( - choices=IPADDRESS_ROLE_CHOICES + choices=IPAddressRoleChoices ) tag = TagFilter() diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e2f5140d2..5bf620a5e 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -769,7 +769,7 @@ class IPAddressCSVForm(forms.ModelForm): help_text='Operational status' ) role = CSVChoiceField( - choices=IPADDRESS_ROLE_CHOICES, + choices=IPAddressRoleChoices, required=False, help_text='Functional role' ) @@ -899,7 +899,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd widget=StaticSelect2() ) role = forms.ChoiceField( - choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), + choices=add_blank_choice(IPAddressRoleChoices), required=False, widget=StaticSelect2() ) @@ -978,7 +978,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo widget=StaticSelect2Multiple() ) role = forms.MultipleChoiceField( - choices=IPADDRESS_ROLE_CHOICES, + choices=IPAddressRoleChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py index ae5ede5e9..e5556900e 100644 --- a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py +++ b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py @@ -8,6 +8,17 @@ IPADDRESS_STATUS_CHOICES = ( (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') @@ -15,6 +26,12 @@ def ipaddress_status_to_slug(apps, schema_editor): 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 @@ -34,4 +51,19 @@ class Migration(migrations.Migration): 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/models.py b/netbox/ipam/models.py index 631805cb1..51431b483 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -21,6 +21,17 @@ from .querysets import PrefixQuerySet from .validators import DNSValidator +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 @@ -565,11 +576,10 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): 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( @@ -620,6 +630,17 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): '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' @@ -756,7 +777,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): 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): 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/views.py b/netbox/ipam/views.py index ce1d9bca7..8bd568df2 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 .choices import PrefixStatusChoices +from .choices import * from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -666,8 +666,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 From 213bd1555a374dcbc1354e602e66d776cf50994e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Nov 2019 22:15:59 -0500 Subject: [PATCH 32/47] VLAN.status to slug (#3569) --- netbox/ipam/api/serializers.py | 2 +- netbox/ipam/choices.py | 23 ++++++++++++ netbox/ipam/constants.py | 20 ----------- netbox/ipam/filters.py | 2 +- netbox/ipam/fixtures/ipam.json | 2 +- netbox/ipam/forms.py | 6 ++-- .../ipam/migrations/0030_3569_vlan_fields.py | 36 +++++++++++++++++++ netbox/ipam/models.py | 16 ++++++--- 8 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 netbox/ipam/migrations/0030_3569_vlan_fields.py diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 8792063aa..7aeb37a45 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -103,7 +103,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) diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index daf8aebec..840a9d8df 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -85,3 +85,26 @@ class IPAddressRoleChoices(ChoiceSet): 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, + } diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index 59e25489c..e68213550 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -4,26 +4,6 @@ AF_CHOICES = ( (6, 'IPv6'), ) -# 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', -} - # IP protocols (for services) IP_PROTOCOL_TCP = 6 diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 26e5297bd..148b40269 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -425,7 +425,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet): 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 9d8da496c..e722b3629 100644 --- a/netbox/ipam/fixtures/ipam.json +++ b/netbox/ipam/fixtures/ipam.json @@ -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 5bf620a5e..bacfbfcff 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1112,7 +1112,7 @@ class VLANCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=VLAN_STATUS_CHOICES, + choices=VLANStatusChoices, help_text='Operational status' ) role = forms.ModelChoiceField( @@ -1181,7 +1181,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() ) @@ -1230,7 +1230,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ) ) status = forms.MultipleChoiceField( - choices=VLAN_STATUS_CHOICES, + choices=VLANStatusChoices, required=False, widget=StaticSelect2Multiple() ) 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/models.py b/netbox/ipam/models.py index 51431b483..00d9f5387 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -868,10 +868,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', @@ -894,6 +894,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 = [ @@ -936,7 +942,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 From 4ecbfc4e5efb9461e97f69eff1b034399b99729a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Nov 2019 22:27:06 -0500 Subject: [PATCH 33/47] Service.protocol to slug (#3569) --- netbox/ipam/api/serializers.py | 5 ++- netbox/ipam/choices.py | 20 +++++++++++ netbox/ipam/constants.py | 14 -------- netbox/ipam/filters.py | 1 - netbox/ipam/forms.py | 5 ++- .../migrations/0031_3569_service_fields.py | 35 +++++++++++++++++++ netbox/ipam/models.py | 12 +++++-- netbox/ipam/tests/test_api.py | 18 +++++----- netbox/ipam/tests/test_views.py | 8 ++--- netbox/ipam/views.py | 1 - 10 files changed, 81 insertions(+), 38 deletions(-) delete mode 100644 netbox/ipam/constants.py create mode 100644 netbox/ipam/migrations/0031_3569_service_fields.py diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 7aeb37a45..5ebc52390 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -9,8 +9,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerial from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer from ipam.choices import * -from ipam.constants import * -from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF +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, @@ -240,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 index 840a9d8df..543608b33 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -108,3 +108,23 @@ class VLANStatusChoices(ChoiceSet): 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 e68213550..000000000 --- a/netbox/ipam/constants.py +++ /dev/null @@ -1,14 +0,0 @@ -# IP address families -AF_CHOICES = ( - (4, 'IPv4'), - (6, 'IPv6'), -) - - -# 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 148b40269..341962ad4 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -10,7 +10,6 @@ from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .choices import * -from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index bacfbfcff..07fed4f2f 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -14,7 +14,6 @@ from utilities.forms import ( ) from virtualization.models import VirtualMachine from .choices import * -from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF IP_FAMILY_CHOICES = [ @@ -1293,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() ) @@ -1308,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/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 00d9f5387..79a6f48ad 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -15,12 +15,17 @@ from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from virtualization.models import VirtualMachine from .choices import * -from .constants 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, @@ -975,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_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 8bd568df2..362d6173c 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -15,7 +15,6 @@ from utilities.views import ( from virtualization.models import VirtualMachine from . import filters, forms, tables from .choices import * -from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF From ca11b9a2f5d4d4893b8ae9e4ea35652837cb40c4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Dec 2019 20:40:18 -0500 Subject: [PATCH 34/47] VirtualMachine.status to slug --- netbox/virtualization/api/serializers.py | 4 +-- netbox/virtualization/choices.py | 24 ++++++++++++++ netbox/virtualization/constants.py | 15 --------- netbox/virtualization/filters.py | 4 +-- netbox/virtualization/forms.py | 8 ++--- .../0011_3569_virtualmachine_fields.py | 33 +++++++++++++++++++ netbox/virtualization/models.py | 17 +++++++--- 7 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 netbox/virtualization/choices.py delete mode 100644 netbox/virtualization/constants.py create mode 100644 netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 98cc63226..8603e31d3 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -10,7 +10,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer 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) 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 3eeddd066..000000000 --- a/netbox/virtualization/constants.py +++ /dev/null @@ -1,15 +0,0 @@ -from dcim.choices import DeviceStatusChoices - -# VirtualMachine statuses (replicated from Device statuses) -VM_STATUS_CHOICES = [ - [1, 'Active'], - [0, 'Offline'], - [3, '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 a438d8598..0e21d75f7 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -10,7 +10,7 @@ from tenancy.filtersets import TenancyFilterSet 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): 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 5bfca6654..712c5e1fa 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -15,7 +15,7 @@ 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 = ( @@ -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() ) 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..828fee71c --- /dev/null +++ b/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py @@ -0,0 +1,33 @@ +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 = [ + 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 790c9c190..09df53344 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=1, # TODO: Replace with ChoiceSet value + 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): From 3ff22bea564381850b0bfcf9a2f46ebb81fcd9f0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Dec 2019 20:58:26 -0500 Subject: [PATCH 35/47] CustomField.type to slug --- netbox/extras/api/customfields.py | 12 +++---- netbox/extras/choices.py | 33 +++++++++++++++++ netbox/extras/constants.py | 16 --------- netbox/extras/filters.py | 5 +-- netbox/extras/forms.py | 11 +++--- ..._squashed_0010_customfield_filter_logic.py | 2 -- .../0010_customfield_filter_logic.py | 4 +-- .../0029_3569_customfield_fields.py | 36 +++++++++++++++++++ netbox/extras/models.py | 31 +++++++++------- netbox/extras/tests/test_changelog.py | 3 +- netbox/extras/tests/test_customfields.py | 34 +++++++++--------- 11 files changed, 123 insertions(+), 64 deletions(-) create mode 100644 netbox/extras/choices.py create mode 100644 netbox/extras/migrations/0029_3569_customfield_fields.py 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/choices.py b/netbox/extras/choices.py new file mode 100644 index 000000000..ec721c552 --- /dev/null +++ b/netbox/extras/choices.py @@ -0,0 +1,33 @@ +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, + } diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 2b4077372..850167235 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -19,22 +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 diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index b307aa308..a3c488281 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,7 @@ 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 == CF_FILTER_EXACT: queryset = queryset.filter( custom_field_values__field__name=self.field_name, custom_field_values__serialized_value=value diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index efb92b2ce..c6dbeaf8d 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 @@ -35,11 +36,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F 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 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..f153c9d11 100644 --- a/netbox/extras/migrations/0010_customfield_filter_logic.py +++ b/netbox/extras/migrations/0010_customfield_filter_logic.py @@ -2,7 +2,7 @@ # 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 +from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE def is_filterable_to_filter_logic(apps, schema_editor): @@ -10,7 +10,7 @@ def is_filterable_to_filter_logic(apps, schema_editor): CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED) CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE) # 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=CF_FILTER_EXACT) def filter_logic_to_is_filterable(apps, schema_editor): 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..436f1c9e8 --- /dev/null +++ b/netbox/extras/migrations/0029_3569_customfield_fields.py @@ -0,0 +1,36 @@ +from django.db import migrations, models + + +CUSTOMFIELD_TYPE_CHOICES = ( + (100, 'text'), + (200, 'integer'), + (300, 'boolean'), + (400, 'date'), + (500, 'url'), + (600, 'select') +) + + +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) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('extras', '0028_remove_topology_maps'), + ] + + operations = [ + migrations.AlterField( + model_name='customfield', + name='type', + field=models.CharField(default='text', max_length=50), + ), + migrations.RunPython( + code=customfield_type_to_slug + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 8f652f4eb..163258b15 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 @@ -182,9 +183,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, @@ -233,15 +235,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 +254,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 +314,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 +332,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() # diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index 22b4912b9..961adfd40 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 ) 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) From bfea77baa504cac8af9f3356fb6df2aef6ccc6b9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Dec 2019 21:09:02 -0500 Subject: [PATCH 36/47] CustomField.filter_logic to slug --- netbox/extras/choices.py | 19 ++++++++++++++ netbox/extras/constants.py | 10 -------- netbox/extras/filters.py | 9 +++++-- netbox/extras/forms.py | 2 +- .../0010_customfield_filter_logic.py | 12 ++++----- .../0029_3569_customfield_fields.py | 25 +++++++++++++++++++ netbox/extras/models.py | 7 +++--- 7 files changed, 61 insertions(+), 23 deletions(-) diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index ec721c552..1ae508c4a 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -31,3 +31,22 @@ class CustomFieldTypeChoices(ChoiceSet): 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, + } diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 850167235..4d4e8835e 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -19,16 +19,6 @@ CUSTOMFIELD_MODELS = [ 'virtualization.virtualmachine', ] -# 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', diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index a3c488281..9e954eaf1 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -43,7 +43,8 @@ class CustomFieldFilter(django_filters.Filter): return queryset.none() # Apply the assigned filter logic (exact or loose) - if self.cf_type == CustomFieldTypeChoices.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 @@ -66,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 c6dbeaf8d..34583eb0d 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -29,7 +29,7 @@ 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)) diff --git a/netbox/extras/migrations/0010_customfield_filter_logic.py b/netbox/extras/migrations/0010_customfield_filter_logic.py index f153c9d11..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 - 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=600).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 index 436f1c9e8..7eced66e9 100644 --- a/netbox/extras/migrations/0029_3569_customfield_fields.py +++ b/netbox/extras/migrations/0029_3569_customfield_fields.py @@ -10,6 +10,12 @@ CUSTOMFIELD_TYPE_CHOICES = ( (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') @@ -17,6 +23,12 @@ def customfield_type_to_slug(apps, schema_editor): 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 @@ -25,6 +37,8 @@ class Migration(migrations.Migration): ] operations = [ + + # CustomField.type migrations.AlterField( model_name='customfield', name='type', @@ -33,4 +47,15 @@ class Migration(migrations.Migration): migrations.RunPython( code=customfield_type_to_slug ), + + # 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/models.py b/netbox/extras/models.py index 163258b15..8278cc19a 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -207,9 +207,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.' ) From 7a3c725f51e06822bc1926ca335b50b515278441 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 15:59:16 -0500 Subject: [PATCH 37/47] Convert BUTTON_CLASS_CHOICES to a ChoiceSet --- netbox/extras/choices.py | 25 +++++++++++++++++++++++++ netbox/extras/constants.py | 17 ----------------- netbox/extras/models.py | 4 ++-- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 1ae508c4a..962a5395b 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -50,3 +50,28 @@ class CustomFieldFilterLogicChoices(ChoiceSet): 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)'), + ) diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 4d4e8835e..f22fb1dd9 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -42,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 diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 8278cc19a..07fdb86eb 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -387,8 +387,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( From 4e1ee270cf22c3b8b3969266b75e1e89330a3a5d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 16:02:52 -0500 Subject: [PATCH 38/47] Extend CustomField migration to update CustomFieldChoice.field.limit_choices_to --- netbox/extras/migrations/0029_3569_customfield_fields.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/extras/migrations/0029_3569_customfield_fields.py b/netbox/extras/migrations/0029_3569_customfield_fields.py index 7eced66e9..23174a4c7 100644 --- a/netbox/extras/migrations/0029_3569_customfield_fields.py +++ b/netbox/extras/migrations/0029_3569_customfield_fields.py @@ -1,4 +1,5 @@ from django.db import migrations, models +import django.db.models.deletion CUSTOMFIELD_TYPE_CHOICES = ( @@ -48,6 +49,13 @@ class Migration(migrations.Migration): 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', From 8efde811e910a14fe04d41ee0c04cde0eb2df237 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 16:05:45 -0500 Subject: [PATCH 39/47] Fix PowerFeed field defaults --- netbox/dcim/migrations/0084_3569_powerfeed_fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py index 6a3ba1ce5..332443d0a 100644 --- a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -61,7 +61,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='powerfeed', name='status', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(default='active', max_length=50), ), migrations.RunPython( code=powerfeed_status_to_slug @@ -71,7 +71,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='powerfeed', name='type', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(default='primary', max_length=50), ), migrations.RunPython( code=powerfeed_type_to_slug @@ -81,7 +81,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='powerfeed', name='supply', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(default='ac', max_length=50), ), migrations.RunPython( code=powerfeed_supply_to_slug @@ -91,7 +91,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='powerfeed', name='phase', - field=models.CharField(blank=True, max_length=50), + field=models.CharField(default='single-phase', max_length=50), ), migrations.RunPython( code=powerfeed_phase_to_slug From 2bcbcd3458094dc26792f54f19ab8ce72d2c1cd7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 16:30:15 -0500 Subject: [PATCH 40/47] ObjectChange.action to slug (#3569) --- netbox/extras/api/serializers.py | 3 +- netbox/extras/choices.py | 23 ++++++++++++ netbox/extras/constants.py | 10 ----- netbox/extras/forms.py | 2 +- netbox/extras/middleware.py | 14 +++---- .../0030_3569_objectchange_fields.py | 37 +++++++++++++++++++ netbox/extras/models.py | 5 ++- netbox/extras/tests/test_changelog.py | 6 +-- netbox/extras/tests/test_views.py | 4 +- netbox/extras/webhooks.py | 7 ++-- netbox/extras/webhooks_worker.py | 2 +- 11 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 netbox/extras/migrations/0030_3569_objectchange_fields.py diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 7c533a5b4..b38ac98fd 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, @@ -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 index 962a5395b..856798ea0 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -75,3 +75,26 @@ class CustomLinkButtonClassChoices(ChoiceSet): (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, + } diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index f22fb1dd9..d033395af 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -93,16 +93,6 @@ TEMPLATE_LANGUAGE_CHOICES = ( (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 diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 34583eb0d..b9f2d1538 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -401,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/0030_3569_objectchange_fields.py b/netbox/extras/migrations/0030_3569_objectchange_fields.py new file mode 100644 index 000000000..de6c616f7 --- /dev/null +++ b/netbox/extras/migrations/0030_3569_objectchange_fields.py @@ -0,0 +1,37 @@ +from django.db import migrations, models +import django.db.models.deletion + + +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/models.py b/netbox/extras/models.py index 07fdb86eb..dc0f6210e 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -802,8 +802,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/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index 961adfd40..8f01cc3bf 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -50,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']) @@ -82,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']) @@ -111,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_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..bafb8e32c 100644 --- a/netbox/extras/webhooks_worker.py +++ b/netbox/extras/webhooks_worker.py @@ -15,7 +15,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': event, 'timestamp': timestamp, 'model': model_name, 'username': username, From a2b0da260877313621a2e0f06c552f0233204eab Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 16:37:22 -0500 Subject: [PATCH 41/47] Fix changelog table action labels --- netbox/extras/tables.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 %} """ From 33890e6b97d55798b440469bd0cb447b5df7f2cd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 16:42:10 -0500 Subject: [PATCH 42/47] Remain consistent with original action strings (e.g. 'created' instead of 'create') --- netbox/extras/webhooks_worker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py index bafb8e32c..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': event, + 'event': dict(ObjectChangeActionChoices)[event].lower(), 'timestamp': timestamp, 'model': model_name, 'username': username, From 2583823e5fc706a0d218aa6e0b851ae59041d3bc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 16:50:44 -0500 Subject: [PATCH 43/47] Delete obsolete user action types --- netbox/extras/constants.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index d033395af..964577295 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -93,24 +93,6 @@ TEMPLATE_LANGUAGE_CHOICES = ( (TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'), ) -# 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 From 89e720cb7712d48dce40e6ecbc6d9fd5b4d2c38f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 17:01:00 -0500 Subject: [PATCH 44/47] ExportTemplate.template_language to slug (#3569) --- netbox/extras/api/serializers.py | 4 +-- netbox/extras/choices.py | 20 +++++++++++ netbox/extras/constants.py | 8 ----- .../0030_3569_objectchange_fields.py | 1 - .../0031_3569_exporttemplate_fields.py | 35 +++++++++++++++++++ netbox/extras/models.py | 7 ++-- 6 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 netbox/extras/migrations/0031_3569_exporttemplate_fields.py diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index b38ac98fd..f648c2187 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -57,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: diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 856798ea0..af5dedb82 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -98,3 +98,23 @@ class ObjectChangeActionChoices(ChoiceSet): 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, + } diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 964577295..f087648fb 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -85,14 +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'), -) - # Report logging levels LOG_DEFAULT = 0 LOG_SUCCESS = 10 diff --git a/netbox/extras/migrations/0030_3569_objectchange_fields.py b/netbox/extras/migrations/0030_3569_objectchange_fields.py index de6c616f7..c31f925e2 100644 --- a/netbox/extras/migrations/0030_3569_objectchange_fields.py +++ b/netbox/extras/migrations/0030_3569_objectchange_fields.py @@ -1,5 +1,4 @@ from django.db import migrations, models -import django.db.models.deletion OBJECTCHANGE_ACTION_CHOICES = ( 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/models.py b/netbox/extras/models.py index dc0f6210e..512eca3ea 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -464,9 +464,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.' From 5d772d7055ad782408248116fc97b331d3087100 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 17:11:59 -0500 Subject: [PATCH 45/47] Webhook.http_content_type to slug (#3569) --- netbox/extras/choices.py | 20 +++++++++++ netbox/extras/constants.py | 8 ----- .../migrations/0032_3569_webhook_fields.py | 35 +++++++++++++++++++ netbox/extras/models.py | 7 ++-- 4 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 netbox/extras/migrations/0032_3569_webhook_fields.py diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index af5dedb82..b483d098f 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -118,3 +118,23 @@ class ExportTemplateLanguageChoices(ChoiceSet): 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 f087648fb..c64715203 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -99,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/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 512eca3ea..48d61ee0f 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -63,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( From edbf56280345fa11d3dd2f3845f14d258d7abe36 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 17:42:33 -0500 Subject: [PATCH 46/47] Annotate all migration operation lists --- netbox/circuits/migrations/0016_3569_circuit_fields.py | 3 +++ netbox/dcim/migrations/0078_3569_site_fields.py | 3 +++ netbox/dcim/migrations/0079_3569_rack_fields.py | 1 + netbox/dcim/migrations/0080_3569_devicetype_fields.py | 3 +++ netbox/dcim/migrations/0082_3569_interface_fields.py | 7 +++++++ netbox/dcim/migrations/0082_3569_port_fields.py | 8 ++++++++ netbox/ipam/migrations/0028_3569_prefix_fields.py | 3 +++ .../migrations/0011_3569_virtualmachine_fields.py | 3 +++ 8 files changed, 31 insertions(+) diff --git a/netbox/circuits/migrations/0016_3569_circuit_fields.py b/netbox/circuits/migrations/0016_3569_circuit_fields.py index f70698c23..a65f72d61 100644 --- a/netbox/circuits/migrations/0016_3569_circuit_fields.py +++ b/netbox/circuits/migrations/0016_3569_circuit_fields.py @@ -25,6 +25,8 @@ class Migration(migrations.Migration): ] operations = [ + + # Circuit.status migrations.AlterField( model_name='circuit', name='status', @@ -33,4 +35,5 @@ class Migration(migrations.Migration): migrations.RunPython( code=circuit_status_to_slug ), + ] diff --git a/netbox/dcim/migrations/0078_3569_site_fields.py b/netbox/dcim/migrations/0078_3569_site_fields.py index 502ad6214..8775abe5e 100644 --- a/netbox/dcim/migrations/0078_3569_site_fields.py +++ b/netbox/dcim/migrations/0078_3569_site_fields.py @@ -21,6 +21,8 @@ class Migration(migrations.Migration): ] operations = [ + + # Site.status migrations.AlterField( model_name='site', name='status', @@ -29,4 +31,5 @@ class Migration(migrations.Migration): 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 index a33b83eb7..4e76a270f 100644 --- a/netbox/dcim/migrations/0079_3569_rack_fields.py +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -88,4 +88,5 @@ class Migration(migrations.Migration): 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 index afd4dd82b..e729eaa55 100644 --- a/netbox/dcim/migrations/0080_3569_devicetype_fields.py +++ b/netbox/dcim/migrations/0080_3569_devicetype_fields.py @@ -20,6 +20,8 @@ class Migration(migrations.Migration): ] operations = [ + + # DeviceType.subdevice_role migrations.AlterField( model_name='devicetype', name='subdevice_role', @@ -33,4 +35,5 @@ class Migration(migrations.Migration): name='subdevice_role', field=models.CharField(blank=True, max_length=50), ), + ] diff --git a/netbox/dcim/migrations/0082_3569_interface_fields.py b/netbox/dcim/migrations/0082_3569_interface_fields.py index b3606d32a..57701ce0a 100644 --- a/netbox/dcim/migrations/0082_3569_interface_fields.py +++ b/netbox/dcim/migrations/0082_3569_interface_fields.py @@ -108,6 +108,8 @@ class Migration(migrations.Migration): ] operations = [ + + # InterfaceTemplate.type migrations.AlterField( model_name='interfacetemplate', name='type', @@ -116,6 +118,8 @@ class Migration(migrations.Migration): migrations.RunPython( code=interfacetemplate_type_to_slug ), + + # Interface.type migrations.AlterField( model_name='interface', name='type', @@ -124,6 +128,8 @@ class Migration(migrations.Migration): migrations.RunPython( code=interface_type_to_slug ), + + # Interface.mode migrations.AlterField( model_name='interface', name='mode', @@ -137,4 +143,5 @@ class Migration(migrations.Migration): 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 index 9cabc3bd5..6d8f50c32 100644 --- a/netbox/dcim/migrations/0082_3569_port_fields.py +++ b/netbox/dcim/migrations/0082_3569_port_fields.py @@ -50,6 +50,8 @@ class Migration(migrations.Migration): ] operations = [ + + # FrontPortTemplate.type migrations.AlterField( model_name='frontporttemplate', name='type', @@ -58,6 +60,8 @@ class Migration(migrations.Migration): migrations.RunPython( code=frontporttemplate_type_to_slug ), + + # RearPortTemplate.type migrations.AlterField( model_name='rearporttemplate', name='type', @@ -66,6 +70,8 @@ class Migration(migrations.Migration): migrations.RunPython( code=rearporttemplate_type_to_slug ), + + # FrontPort.type migrations.AlterField( model_name='frontport', name='type', @@ -74,6 +80,8 @@ class Migration(migrations.Migration): migrations.RunPython( code=frontport_type_to_slug ), + + # RearPort.type migrations.AlterField( model_name='rearport', name='type', diff --git a/netbox/ipam/migrations/0028_3569_prefix_fields.py b/netbox/ipam/migrations/0028_3569_prefix_fields.py index 1aaa9c22b..f0db5f403 100644 --- a/netbox/ipam/migrations/0028_3569_prefix_fields.py +++ b/netbox/ipam/migrations/0028_3569_prefix_fields.py @@ -23,6 +23,8 @@ class Migration(migrations.Migration): ] operations = [ + + # Prefix.status migrations.AlterField( model_name='prefix', name='status', @@ -31,4 +33,5 @@ class Migration(migrations.Migration): migrations.RunPython( code=prefix_status_to_slug ), + ] diff --git a/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py b/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py index 828fee71c..869975075 100644 --- a/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py +++ b/netbox/virtualization/migrations/0011_3569_virtualmachine_fields.py @@ -22,6 +22,8 @@ class Migration(migrations.Migration): ] operations = [ + + # VirtualMachine.status migrations.AlterField( model_name='virtualmachine', name='status', @@ -30,4 +32,5 @@ class Migration(migrations.Migration): migrations.RunPython( code=virtualmachine_status_to_slug ), + ] From b0c0adf6e72dcb63ddfb6538d3bcbc47d51ff199 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Dec 2019 17:49:44 -0500 Subject: [PATCH 47/47] Adapt device component import forms from #3711 --- netbox/dcim/forms.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b52731f92..9a836326a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2371,7 +2371,7 @@ class PowerOutletCSVForm(forms.ModelForm): } ) feed_leg = CSVChoiceField( - choices=POWERFEED_LEG_CHOICES, + choices=PowerOutletFeedLegChoices, required=False, ) @@ -2690,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, ) @@ -2715,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() @@ -2966,7 +2966,7 @@ class FrontPortCSVForm(forms.ModelForm): } ) type = CSVChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, ) class Meta: @@ -3084,7 +3084,7 @@ class RearPortCSVForm(forms.ModelForm): } ) type = CSVChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, ) class Meta: @@ -3708,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()