Skip to content

Commit

Permalink
Closes netbox-community#241: Introduced rack roles
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Aug 10, 2016
1 parent 5971a96 commit 3019449
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 24 deletions.
12 changes: 10 additions & 2 deletions netbox/dcim/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
)


Expand All @@ -24,9 +24,17 @@ class RackGroupAdmin(admin.ModelAdmin):
}


@admin.register(RackRole)
class RackRoleAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'color']
prepopulated_fields = {
'slug': ['name'],
}


@admin.register(Rack)
class RackAdmin(admin.ModelAdmin):
list_display = ['name', 'facility_id', 'site', 'type', 'width', 'u_height']
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']


#
Expand Down
28 changes: 23 additions & 5 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
)
from tenancy.api.serializers import TenantNestedSerializer

Expand Down Expand Up @@ -46,6 +46,23 @@ class Meta(SiteSerializer.Meta):
fields = ['id', 'name', 'slug']


#
# Rack roles
#

class RackRoleSerializer(serializers.ModelSerializer):

class Meta:
model = RackRole
fields = ['id', 'name', 'slug', 'color']


class RackRoleNestedSerializer(RackRoleSerializer):

class Meta(RackRoleSerializer.Meta):
fields = ['id', 'name', 'slug']


#
# Racks
#
Expand All @@ -55,11 +72,12 @@ class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer()
role = RackRoleNestedSerializer()

class Meta:
model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height',
'comments']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'comments']


class RackNestedSerializer(RackSerializer):
Expand All @@ -73,8 +91,8 @@ class RackDetailSerializer(RackSerializer):
rear_units = serializers.SerializerMethodField()

class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height',
'comments', 'front_units', 'rear_units']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'comments', 'front_units', 'rear_units']

def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT)
Expand Down
4 changes: 4 additions & 0 deletions netbox/dcim/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),

# Rack roles
url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),

# Racks
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
Expand Down
22 changes: 21 additions & 1 deletion netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
)
from dcim import filters
from .exceptions import MissingFilterException
Expand Down Expand Up @@ -60,6 +60,26 @@ class RackGroupDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RackGroupSerializer


#
# Rack roles
#

class RackRoleListView(generics.ListAPIView):
"""
List all rack roles
"""
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer


class RackRoleDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack role
"""
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer


#
# Racks
#
Expand Down
13 changes: 12 additions & 1 deletion netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
)
from tenancy.models import Tenant

Expand Down Expand Up @@ -96,6 +96,17 @@ class RackFilter(django_filters.FilterSet):
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)

class Meta:
model = Rack
Expand Down
58 changes: 52 additions & 6 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, Site,
STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
)


Expand Down Expand Up @@ -50,6 +50,30 @@ def bulkedit_platform_choices():
return choices


def bulkedit_rackgroup_choices():
"""
Include an option to remove the currently assigned group from a rack.
"""
choices = [
(None, '---------'),
(0, 'None'),
]
choices += [(r.pk, r) for r in RackGroup.objects.all()]
return choices


def bulkedit_rackrole_choices():
"""
Include an option to remove the currently assigned role from a rack.
"""
choices = [
(None, '---------'),
(0, 'None'),
]
choices += [(r.pk, r.name) for r in RackRole.objects.all()]
return choices


#
# Sites
#
Expand Down Expand Up @@ -124,6 +148,18 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8}))


#
# Rack roles
#

class RackRoleForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()

class Meta:
model = RackRole
fields = ['name', 'slug', 'color']


#
# Racks
#
Expand All @@ -136,7 +172,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):

class Meta:
model = Rack
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'type', 'width', 'u_height', 'comments']
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments']
help_texts = {
'site': "The site at which the rack exists",
'name': "Organizational rack name",
Expand Down Expand Up @@ -166,11 +202,13 @@ class RackFromCSVForm(forms.ModelForm):
group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Role not found.'})
type = forms.CharField(required=False)

class Meta:
model = Rack
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'type', 'width', 'u_height']
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']

def clean(self):

Expand Down Expand Up @@ -204,9 +242,10 @@ class RackImportForm(BulkImportForm, BootstrapMixin):

class RackBulkEditForm(forms.Form, BootstrapMixin):
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)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
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)')
Expand All @@ -228,13 +267,20 @@ def rack_tenant_choices():
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]


def rack_role_choices():
role_choices = RackRole.objects.annotate(rack_count=Count('racks'))
return [(r.slug, u'{} ({})'.format(r.name, r.rack_count)) for r in role_choices]


class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))


#
Expand Down
33 changes: 33 additions & 0 deletions netbox/dcim/migrations/0017_rack_add_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-10 14:58
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('dcim', '0016_module_add_manufacturer'),
]

operations = [
migrations.CreateModel(
name='RackRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='rack',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
),
]
34 changes: 32 additions & 2 deletions netbox/dcim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
COLOR_GRAY1 = 'light_gray'
COLOR_GRAY2 = 'medium_gray'
COLOR_GRAY3 = 'dark_gray'
DEVICE_ROLE_COLOR_CHOICES = [
ROLE_COLOR_CHOICES = [
[COLOR_TEAL, 'Teal'],
[COLOR_GREEN, 'Green'],
[COLOR_BLUE, 'Blue'],
Expand Down Expand Up @@ -203,6 +203,10 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
}).order_by(*ordering)


#
# Sites
#

class SiteManager(NaturalOrderByManager):

def get_queryset(self):
Expand Down Expand Up @@ -264,6 +268,10 @@ def count_circuits(self):
return self.circuits.count()


#
# Racks
#

class RackGroup(models.Model):
"""
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
Expand All @@ -288,6 +296,24 @@ def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)


class RackRole(models.Model):
"""
Racks can be organized by functional role, similar to Devices.
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)

class Meta:
ordering = ['name']

def __unicode__(self):
return self.name

def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)


class RackManager(NaturalOrderByManager):

def get_queryset(self):
Expand All @@ -304,6 +330,7 @@ class Rack(CreatedUpdatedModel):
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
help_text='Rail-to-rail width')
Expand Down Expand Up @@ -344,6 +371,9 @@ def to_csv(self):
self.name,
self.facility_id or '',
self.tenant.name if self.tenant else '',
self.role.name if self.role else '',
self.get_type_display() if self.type else '',
self.width,
str(self.u_height),
])

Expand Down Expand Up @@ -651,7 +681,7 @@ class DeviceRole(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=DEVICE_ROLE_COLOR_CHOICES)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)

class Meta:
ordering = ['name']
Expand Down
Loading

0 comments on commit 3019449

Please sign in to comment.