1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #2104: Add 'status' field to Rack

This commit is contained in:
Jeremy Stretch
2018-11-01 16:03:42 -04:00
parent 91ce6c2420
commit fa6c4db13b
10 changed files with 124 additions and 37 deletions

View File

@ -117,6 +117,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer() site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True) role = NestedRackRoleSerializer(required=False, allow_null=True)
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
@ -125,8 +126,8 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial', 'type',
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '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 # Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
# prevents facility_id from being interpreted as a required field. # prevents facility_id from being interpreted as a required field.

View File

@ -29,6 +29,20 @@ RACK_FACE_CHOICES = [
[RACK_FACE_REAR, 'Rear'], [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 # Parent/child device roles
SUBDEVICE_ROLE_PARENT = True SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False SUBDEVICE_ROLE_CHILD = False
@ -265,7 +279,7 @@ SITE_STATUS_CHOICES = [
[SITE_STATUS_RETIRED, 'Retired'], [SITE_STATUS_RETIRED, 'Retired'],
] ]
# Bootstrap CSS classes for device statuses # Bootstrap CSS classes for device/rack statuses
STATUS_CLASSES = { STATUS_CLASSES = {
0: 'warning', 0: 'warning',
1: 'success', 1: 'success',

View File

@ -8,10 +8,7 @@ from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NumericInFilter from utilities.filters import NullableCharFieldFilter, NumericInFilter
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import ( from .constants import *
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
WIRELESS_IFACE_TYPES,
)
from .models import ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@ -183,6 +180,10 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
status = django_filters.MultipleChoiceFilter(
choices=RACK_STATUS_CHOICES,
null_value=None
)
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
label='Role (ID)', label='Role (ID)',

View File

@ -306,8 +306,8 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'type',
'u_height', 'desc_units', 'comments', 'tags', 'width', 'u_height', 'desc_units', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'site': "The site at which the rack exists", 'site': "The site at which the rack exists",
@ -342,6 +342,11 @@ class RackCSVForm(forms.ModelForm):
'invalid_choice': 'Tenant not found.', 'invalid_choice': 'Tenant not found.',
} }
) )
status = CSVChoiceField(
choices=RACK_STATUS_CHOICES,
required=False,
help_text='Operational status'
)
role = forms.ModelChoiceField( role = forms.ModelChoiceField(
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
required=False, required=False,
@ -402,17 +407,56 @@ class RackCSVForm(forms.ModelForm):
class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site') queryset=Rack.objects.all(),
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group') widget=forms.MultipleHiddenInput
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) )
role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False) site = forms.ModelChoiceField(
serial = forms.CharField(max_length=50, required=False, label='Serial Number') queryset=Site.objects.all(),
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') required=False
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') )
u_height = forms.IntegerField(required=False, label='Height (U)') group = forms.ModelChoiceField(
desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units') queryset=RackGroup.objects.all(),
comments = CommentField(widget=SmallTextarea) 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: class Meta:
nullable_fields = ['group', 'tenant', 'role', 'serial', 'comments'] nullable_fields = ['group', 'tenant', 'role', 'serial', 'comments']
@ -435,6 +479,12 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --'
) )
status = AnnotatedMultipleChoiceField(
choices=RACK_STATUS_CHOICES,
annotate=Rack.objects.all(),
annotate_field='status',
required=False
)
role = FilterChoiceField( role = FilterChoiceField(
queryset=RackRole.objects.annotate(filter_count=Count('racks')), queryset=RackRole.objects.annotate(filter_count=Count('racks')),
to_field_name='slug', to_field_name='slug',

View File

@ -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),
),
]

View File

@ -464,6 +464,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
blank=True, blank=True,
null=True null=True
) )
status = models.PositiveSmallIntegerField(
choices=RACK_STATUS_CHOICES,
default=RACK_STATUS_ACTIVE
)
role = models.ForeignKey( role = models.ForeignKey(
to='dcim.RackRole', to='dcim.RackRole',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -514,7 +518,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager() tags = TaggableManager()
csv_headers = [ 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', 'desc_units', 'comments',
] ]
@ -571,6 +575,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
self.name, self.name,
self.facility_id, self.facility_id,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.get_status_display(),
self.role.name if self.role else None, self.role.name if self.role else None,
self.get_type_display() if self.type else None, self.get_type_display() if self.type else None,
self.serial, self.serial,
@ -595,6 +600,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
return self.name return self.name
return "" 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): 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'} Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}

View File

@ -274,12 +274,13 @@ class RackTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.TemplateColumn(template_code=COL_TENANT) tenant = tables.TemplateColumn(template_code=COL_TENANT)
status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(RACK_ROLE) role = tables.TemplateColumn(RACK_ROLE)
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Rack 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): class RackDetailTable(RackTable):
@ -291,24 +292,11 @@ class RackDetailTable(RackTable):
class Meta(RackTable.Meta): class Meta(RackTable.Meta):
fields = ( 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', '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 # Rack reservations
# #

View File

@ -401,7 +401,7 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackBulkImportView(PermissionRequiredMixin, BulkImportView): class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rack' permission_required = 'dcim.add_rack'
model_form = forms.RackCSVForm model_form = forms.RackCSVForm
table = tables.RackImportTable table = tables.RackTable
default_return_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'

View File

@ -105,6 +105,12 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Status</td>
<td>
{{ rack.get_status_display }}
</td>
</tr>
<tr> <tr>
<td>Role</td> <td>Role</td>
<td> <td>

View File

@ -9,6 +9,7 @@
{% render_field form.name %} {% render_field form.name %}
{% render_field form.facility_id %} {% render_field form.facility_id %}
{% render_field form.group %} {% render_field form.group %}
{% render_field form.status %}
{% render_field form.role %} {% render_field form.role %}
{% render_field form.serial %} {% render_field form.serial %}
</div> </div>