diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 63362a71d..b43883f40 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -117,6 +117,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) role = NestedRackRoleSerializer(required=False, allow_null=True) type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) @@ -125,8 +126,8 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): class Meta: model = Rack fields = [ - 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', - 'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial', 'type', + 'width', 'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] # Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This # prevents facility_id from being interpreted as a required field. diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 3fd75bfc9..4e2050d44 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -29,6 +29,20 @@ 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'], +] + # Parent/child device roles SUBDEVICE_ROLE_PARENT = True SUBDEVICE_ROLE_CHILD = False @@ -265,7 +279,7 @@ SITE_STATUS_CHOICES = [ [SITE_STATUS_RETIRED, 'Retired'], ] -# Bootstrap CSS classes for device statuses +# Bootstrap CSS classes for device/rack statuses STATUS_CLASSES = { 0: 'warning', 1: 'success', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index a211fd60e..b7d69a338 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,10 +8,7 @@ from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.filters import NullableCharFieldFilter, NumericInFilter from virtualization.models import Cluster -from .constants import ( - DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES, - WIRELESS_IFACE_TYPES, -) +from .constants import * from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, @@ -183,6 +180,10 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) + status = django_filters.MultipleChoiceFilter( + choices=RACK_STATUS_CHOICES, + null_value=None + ) role_id = django_filters.ModelMultipleChoiceFilter( queryset=RackRole.objects.all(), label='Role (ID)', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c16412927..6e6abfd15 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -306,8 +306,8 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = Rack fields = [ - 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width', - 'u_height', 'desc_units', 'comments', 'tags', + 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'type', + 'width', 'u_height', 'desc_units', 'comments', 'tags', ] help_texts = { 'site': "The site at which the rack exists", @@ -342,6 +342,11 @@ class RackCSVForm(forms.ModelForm): 'invalid_choice': 'Tenant not found.', } ) + status = CSVChoiceField( + choices=RACK_STATUS_CHOICES, + required=False, + help_text='Operational status' + ) role = forms.ModelChoiceField( queryset=RackRole.objects.all(), required=False, @@ -402,17 +407,56 @@ class RackCSVForm(forms.ModelForm): class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): - pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) - site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site') - group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group') - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) - role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False) - serial = forms.CharField(max_length=50, required=False, label='Serial Number') - type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') - width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') - u_height = forms.IntegerField(required=False, label='Height (U)') - desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units') - comments = CommentField(widget=SmallTextarea) + pk = forms.ModelMultipleChoiceField( + queryset=Rack.objects.all(), + widget=forms.MultipleHiddenInput + ) + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + required=False + ) + group = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + required=False + ) + tenant = forms.ModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + status = forms.ChoiceField( + choices=add_blank_choice(RACK_STATUS_CHOICES), + required=False, + initial='' + ) + role = forms.ModelChoiceField( + queryset=RackRole.objects.all(), + required=False + ) + serial = forms.CharField( + max_length=50, + required=False, + label='Serial Number' + ) + type = forms.ChoiceField( + choices=add_blank_choice(RACK_TYPE_CHOICES), + required=False + ) + width = forms.ChoiceField( + choices=add_blank_choice(RACK_WIDTH_CHOICES), + required=False + ) + u_height = forms.IntegerField( + required=False, + label='Height (U)' + ) + desc_units = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect, + label='Descending units' + ) + comments = CommentField( + widget=SmallTextarea + ) class Meta: nullable_fields = ['group', 'tenant', 'role', 'serial', 'comments'] @@ -435,6 +479,12 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --' ) + status = AnnotatedMultipleChoiceField( + choices=RACK_STATUS_CHOICES, + annotate=Rack.objects.all(), + annotate_field='status', + required=False + ) role = FilterChoiceField( queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug', diff --git a/netbox/dcim/migrations/0068_rack_status.py b/netbox/dcim/migrations/0068_rack_status.py new file mode 100644 index 000000000..e190ff7df --- /dev/null +++ b/netbox/dcim/migrations/0068_rack_status.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.9 on 2018-11-01 19:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0067_device_type_remove_qualifiers'), + ] + + operations = [ + migrations.AddField( + model_name='rack', + name='status', + field=models.PositiveSmallIntegerField(default=3), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6e876c360..00e522082 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -464,6 +464,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): blank=True, null=True ) + status = models.PositiveSmallIntegerField( + choices=RACK_STATUS_CHOICES, + default=RACK_STATUS_ACTIVE + ) role = models.ForeignKey( to='dcim.RackRole', on_delete=models.PROTECT, @@ -514,7 +518,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager() csv_headers = [ - 'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height', + 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'width', 'u_height', 'desc_units', 'comments', ] @@ -571,6 +575,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): self.name, self.facility_id, self.tenant.name if self.tenant else None, + self.get_status_display(), self.role.name if self.role else None, self.get_type_display() if self.type else None, self.serial, @@ -595,6 +600,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel): return self.name return "" + def get_status_class(self): + return STATUS_CLASSES[self.status] + def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index e0acab111..d46a32f01 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -274,12 +274,13 @@ class RackTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') tenant = tables.TemplateColumn(template_code=COL_TENANT) + status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') class Meta(BaseTable.Meta): model = Rack - fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height') + fields = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') class RackDetailTable(RackTable): @@ -291,24 +292,11 @@ class RackDetailTable(RackTable): class Meta(RackTable.Meta): fields = ( - 'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', + 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'get_utilization', ) -class RackImportTable(BaseTable): - name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - facility_id = tables.Column(verbose_name='Facility ID') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - u_height = tables.Column(verbose_name='Height (U)') - - class Meta(BaseTable.Meta): - model = Rack - fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height') - - # # Rack reservations # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 7be086229..828271ac6 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -401,7 +401,7 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackBulkImportView(PermissionRequiredMixin, BulkImportView): permission_required = 'dcim.add_rack' model_form = forms.RackCSVForm - table = tables.RackImportTable + table = tables.RackTable default_return_url = 'dcim:rack_list' diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index aaebe02da..52f841eb3 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -105,6 +105,12 @@ {% endif %} + + Status + + {{ rack.get_status_display }} + + Role diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index d500a1954..0326e9523 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -9,6 +9,7 @@ {% render_field form.name %} {% render_field form.facility_id %} {% render_field form.group %} + {% render_field form.status %} {% render_field form.role %} {% render_field form.serial %}