Merge pull request #453 from digitalocean/develop

Release v1.5.0
This commit is contained in:
Jeremy Stretch 2016-08-10 17:47:48 -04:00 committed by GitHub
commit 2509405465
46 changed files with 686 additions and 146 deletions

View File

@ -1,4 +1,4 @@
# NetBox ![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.

View File

@ -10,15 +10,21 @@ Sites can be assigned an optional facility ID to identify the actual facility ho
# Racks # Racks
Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units *(U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted. Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units* (U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted.
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, M204.313) whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number. Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, M204.313) whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
### Rack Groups ### Rack Groups
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room. Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room.
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not currently supported. Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
### Rack Roles
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
--- ---
@ -74,7 +80,7 @@ The assignment of platforms to devices is an entirely optional feature, and may
### Modules ### Modules
A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each module can optionally be assigned to a manufacturer.
### Components ### Components

BIN
docs/netbox_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -21,8 +21,8 @@ class CircuitTypeAdmin(admin.ModelAdmin):
@admin.register(Circuit) @admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin): class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate', list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
'xconnect_id'] 'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
list_filter = ['provider', 'type', 'tenant'] list_filter = ['provider', 'type', 'tenant']
exclude = ['interface'] exclude = ['interface']

View File

@ -53,7 +53,7 @@ class CircuitSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed', fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
'commit_rate', 'xconnect_id', 'comments'] 'upstream_speed', 'commit_rate', 'xconnect_id', 'comments']
class CircuitNestedSerializer(CircuitSerializer): class CircuitNestedSerializer(CircuitSerializer):

View File

@ -102,7 +102,7 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
model = Circuit model = Circuit
fields = [ fields = [
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date', 'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments' 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
] ]
help_texts = { help_texts = {
'cid': "Unique circuit ID", 'cid': "Unique circuit ID",
@ -169,8 +169,8 @@ class CircuitFromCSVForm(forms.ModelForm):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate', fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info'] 'commit_rate', 'xconnect_id', 'pp_info']
class CircuitImportForm(BulkImportForm, BootstrapMixin): class CircuitImportForm(BulkImportForm, BootstrapMixin):

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-08 20:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0004_circuit_add_tenant'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='upstream_speed',
field=models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)'),
),
]

View File

@ -72,6 +72,8 @@ class Circuit(CreatedUpdatedModel):
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True) interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed') install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)') port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
help_text='Upstream speed, if different from port speed')
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)') commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
@ -96,6 +98,7 @@ class Circuit(CreatedUpdatedModel):
self.site.name, self.site.name,
self.install_date.isoformat() if self.install_date else '', self.install_date.isoformat() if self.install_date else '',
str(self.port_speed), str(self.port_speed),
str(self.upstream_speed),
str(self.commit_rate) if self.commit_rate else '', str(self.commit_rate) if self.commit_rate else '',
self.xconnect_id, self.xconnect_id,
self.pp_info, self.pp_info,
@ -116,12 +119,18 @@ class Circuit(CreatedUpdatedModel):
else: else:
return '{} Kbps'.format(speed) return '{} Kbps'.format(speed)
@property
def port_speed_human(self): def port_speed_human(self):
return self._humanize_speed(self.port_speed) return self._humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
if not self.upstream_speed:
return ''
return self._humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'
@property
def commit_rate_human(self): def commit_rate_human(self):
if not self.commit_rate: if not self.commit_rate:
return '' return ''
return self._humanize_speed(self.commit_rate) return self._humanize_speed(self.commit_rate)
commit_rate_human.admin_order_field = 'commit_rate'

View File

@ -4,7 +4,7 @@ from django.db.models import Count
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
) )
@ -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) @admin.register(Rack)
class RackAdmin(admin.ModelAdmin): class RackAdmin(admin.ModelAdmin):
list_display = ['name', 'facility_id', 'site', 'u_height'] list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
# #

View File

@ -3,8 +3,8 @@ from rest_framework import serializers
from ipam.models import IPAddress from ipam.models import IPAddress
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, 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 from tenancy.api.serializers import TenantNestedSerializer
@ -46,6 +46,23 @@ class RackGroupNestedSerializer(RackGroupSerializer):
fields = ['id', 'name', 'slug'] 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 # Racks
# #
@ -55,10 +72,12 @@ class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer() site = SiteNestedSerializer()
group = RackGroupNestedSerializer() group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer() tenant = TenantNestedSerializer()
role = RackRoleNestedSerializer()
class Meta: class Meta:
model = Rack model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments'] fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'comments']
class RackNestedSerializer(RackSerializer): class RackNestedSerializer(RackSerializer):
@ -72,8 +91,8 @@ class RackDetailSerializer(RackSerializer):
rear_units = serializers.SerializerMethodField() rear_units = serializers.SerializerMethodField()
class Meta(RackSerializer.Meta): class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments', fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'front_units', 'rear_units'] 'u_height', 'comments', 'front_units', 'rear_units']
def get_front_units(self, obj): def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT) units = obj.get_rack_units(face=RACK_FACE_FRONT)
@ -384,6 +403,25 @@ class DeviceBayDetailSerializer(DeviceBaySerializer):
fields = ['id', 'device', 'name', 'installed_device'] fields = ['id', 'device', 'name', 'installed_device']
#
# Modules
#
class ModuleSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
manufacturer = ManufacturerNestedSerializer()
class Meta:
model = Module
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
class ModuleNestedSerializer(ModuleSerializer):
class Meta(ModuleSerializer.Meta):
fields = ['id', 'device', 'parent', 'name']
# #
# Interface connections # Interface connections
# #

View File

@ -18,6 +18,10 @@ urlpatterns = [
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'), url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'), 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 # Racks
url(r'^racks/$', RackListView.as_view(), name='rack_list'), url(r'^racks/$', RackListView.as_view(), name='rack_list'),
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'), url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
@ -50,6 +54,7 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'), url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'), url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'), url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
url(r'^devices/(?P<pk>\d+)/modules/$', ModuleListView.as_view(), name='device_modules'),
# Console ports # Console ports
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'), url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),

View File

@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
InterfaceConnection, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site, InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
) )
from dcim import filters from dcim import filters
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
@ -60,6 +60,26 @@ class RackGroupDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RackGroupSerializer 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 # Racks
# #
@ -349,18 +369,23 @@ class DeviceBayListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk']) device = get_object_or_404(Device, pk=self.kwargs['pk'])
queryset = DeviceBay.objects.filter(device=device).select_related('installed_device') return DeviceBay.objects.filter(device=device).select_related('installed_device')
# Filter by type (physical or virtual)
iface_type = self.request.query_params.get('type')
if iface_type == 'physical':
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
elif iface_type == 'virtual':
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
elif iface_type is not None:
queryset = queryset.empty()
return queryset #
# Modules
#
class ModuleListView(generics.ListAPIView):
"""
List device modules (by device)
"""
serializer_class = serializers.ModuleSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return Module.objects.filter(device=device).select_related('device', 'manufacturer')
# #

View File

@ -4,7 +4,7 @@ from django.db.models import Q
from .models import ( from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, 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 from tenancy.models import Tenant
@ -96,6 +96,17 @@ class RackFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (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: class Meta:
model = Rack model = Rack

View File

@ -7,7 +7,7 @@ from ipam.models import IPAddress
from tenancy.forms import bulkedit_tenant_choices from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
) )
@ -15,7 +15,8 @@ from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, 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
) )
@ -49,6 +50,30 @@ def bulkedit_platform_choices():
return 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 # Sites
# #
@ -123,6 +148,18 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Rack roles
#
class RackRoleForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
# #
# Racks # Racks
# #
@ -135,7 +172,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = Rack model = Rack
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'u_height', 'comments'] fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments']
help_texts = { help_texts = {
'site': "The site at which the rack exists", 'site': "The site at which the rack exists",
'name': "Organizational rack name", 'name': "Organizational rack name",
@ -165,10 +202,13 @@ class RackFromCSVForm(forms.ModelForm):
group_name = forms.CharField(required=False) group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}) 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: class Meta:
model = Rack model = Rack
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'u_height'] fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']
def clean(self): def clean(self):
@ -182,6 +222,19 @@ class RackFromCSVForm(forms.ModelForm):
except RackGroup.DoesNotExist: except RackGroup.DoesNotExist:
self.add_error('group_name', "Invalid rack group ({})".format(group)) self.add_error('group_name', "Invalid rack group ({})".format(group))
def clean_type(self):
rack_type = self.cleaned_data['type']
if not rack_type:
return None
try:
choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
return choices[rack_type.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
rack_type,
', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
))
class RackImportForm(BulkImportForm, BootstrapMixin): class RackImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=RackFromCSVForm) csv = CSVDataField(csv_form=RackFromCSVForm)
@ -189,9 +242,12 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
class RackBulkEditForm(forms.Form, BootstrapMixin): class RackBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False) 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') 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)') u_height = forms.IntegerField(required=False, label='Height (U)')
comments = CommentField() comments = CommentField()
@ -211,6 +267,11 @@ def rack_tenant_choices():
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in 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): class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices, site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
@ -218,6 +279,8 @@ class RackFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices, tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -402,7 +465,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
self.fields['primary_ip6'].widget.attrs['readonly'] = True self.fields['primary_ip6'].widget.attrs['readonly'] = True
# Limit rack choices # Limit rack choices
if self.is_bound: if self.is_bound and self.data.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site']) self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'): elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site']) self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
@ -443,6 +506,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True self.fields['site'].disabled = True
self.fields['rack'].disabled = True self.fields['rack'].disabled = True
self.initial['site'] = self.instance.parent_bay.device.rack.site_id
self.initial['rack'] = self.instance.parent_bay.device.rack_id
class BaseDeviceFromCSVForm(forms.ModelForm): class BaseDeviceFromCSVForm(forms.ModelForm):
@ -1254,4 +1319,4 @@ class ModuleForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = Module model = Module
fields = ['name', 'part_id', 'serial'] fields = ['name', 'manufacturer', 'part_id', 'serial']

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-08 21:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0013_add_interface_form_factors'),
]
operations = [
migrations.AddField(
model_name='rack',
name='type',
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
),
migrations.AddField(
model_name='rack',
name='width',
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-09 21:18
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0014_rack_add_type_width'),
]
operations = [
migrations.AlterField(
model_name='rack',
name='u_height',
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-10 13:45
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0015_rack_add_u_height_validator'),
]
operations = [
migrations.AddField(
model_name='module',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
),
]

View File

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

View File

@ -3,7 +3,7 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
@ -16,6 +16,26 @@ from utilities.models import CreatedUpdatedModel
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
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_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
RACK_FACE_FRONT = 0 RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1 RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [ RACK_FACE_CHOICES = [
@ -41,7 +61,7 @@ COLOR_RED = 'red'
COLOR_GRAY1 = 'light_gray' COLOR_GRAY1 = 'light_gray'
COLOR_GRAY2 = 'medium_gray' COLOR_GRAY2 = 'medium_gray'
COLOR_GRAY3 = 'dark_gray' COLOR_GRAY3 = 'dark_gray'
DEVICE_ROLE_COLOR_CHOICES = [ ROLE_COLOR_CHOICES = [
[COLOR_TEAL, 'Teal'], [COLOR_TEAL, 'Teal'],
[COLOR_GREEN, 'Green'], [COLOR_GREEN, 'Green'],
[COLOR_BLUE, 'Blue'], [COLOR_BLUE, 'Blue'],
@ -183,6 +203,10 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
}).order_by(*ordering) }).order_by(*ordering)
#
# Sites
#
class SiteManager(NaturalOrderByManager): class SiteManager(NaturalOrderByManager):
def get_queryset(self): def get_queryset(self):
@ -244,6 +268,10 @@ class Site(CreatedUpdatedModel):
return self.circuits.count() return self.circuits.count()
#
# Racks
#
class RackGroup(models.Model): 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 Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
@ -268,6 +296,24 @@ class RackGroup(models.Model):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) 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): class RackManager(NaturalOrderByManager):
def get_queryset(self): def get_queryset(self):
@ -284,7 +330,12 @@ class Rack(CreatedUpdatedModel):
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) 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) 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) tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)') 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')
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
validators=[MinValueValidator(1), MaxValueValidator(100)])
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
objects = RackManager() objects = RackManager()
@ -320,6 +371,9 @@ class Rack(CreatedUpdatedModel):
self.name, self.name,
self.facility_id or '', self.facility_id or '',
self.tenant.name if self.tenant else '', 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), str(self.u_height),
]) ])
@ -627,7 +681,7 @@ class DeviceRole(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(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: class Meta:
ordering = ['name'] ordering = ['name']
@ -1073,6 +1127,8 @@ class Module(models.Model):
device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='submodules', blank=True, null=True, on_delete=models.CASCADE) parent = models.ForeignKey('self', related_name='submodules', blank=True, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name') name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey('Manufacturer', related_name='modules', blank=True, null=True,
on_delete=models.PROTECT)
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True) part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True) serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
discovered = models.BooleanField(default=False, verbose_name='Discovered') discovered = models.BooleanField(default=False, verbose_name='Discovered')

View File

@ -1,7 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ColorColumn, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
@ -22,6 +22,12 @@ RACKGROUP_ACTIONS = """
{% endif %} {% endif %}
""" """
RACKROLE_ACTIONS = """
{% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICEROLE_ACTIONS = """ DEVICEROLE_ACTIONS = """
{% if perms.dcim.change_devicerole %} {% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@ -94,6 +100,24 @@ class RackGroupTable(BaseTable):
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions') fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
#
# Rack roles
#
class RackRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
rack_count = tables.Column(verbose_name='Racks')
color = ColorColumn(verbose_name='Color')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = RackGroup
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
# #
# Racks # Racks
# #
@ -105,6 +129,7 @@ class RackTable(BaseTable):
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID') facility_id = tables.Column(verbose_name='Facility ID')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
role = tables.Column(verbose_name='Role')
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices') devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used') u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
@ -112,7 +137,7 @@ class RackTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Rack model = Rack
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'u_height', 'devices', 'u_consumed', fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'u_consumed',
'utilization') 'utilization')
@ -233,14 +258,14 @@ class DeviceRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
color = ColorColumn(verbose_name='Color')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
color = tables.Column(verbose_name='Color')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ('pk', 'name', 'device_count', 'slug', 'color', 'actions') fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
# #

View File

@ -42,6 +42,9 @@ class SiteTest(APITestCase):
'site', 'site',
'group', 'group',
'tenant', 'tenant',
'role',
'type',
'width',
'u_height', 'u_height',
'comments' 'comments'
] ]
@ -118,6 +121,9 @@ class RackTest(APITestCase):
'site', 'site',
'group', 'group',
'tenant', 'tenant',
'role',
'type',
'width',
'u_height', 'u_height',
'comments' 'comments'
] ]
@ -130,6 +136,9 @@ class RackTest(APITestCase):
'site', 'site',
'group', 'group',
'tenant', 'tenant',
'role',
'type',
'width',
'u_height', 'u_height',
'comments', 'comments',
'front_units', 'front_units',

View File

@ -13,59 +13,67 @@ class DeviceTestCase(TestCase):
def test_racked_device(self): def test_racked_device(self):
test = DeviceForm(data={ test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'leaf-switch'),
'name': 'test', 'name': 'test',
'site': get_id(Site, 'test1'), 'device_role': get_id(DeviceRole, 'leaf-switch'),
'face': RACK_FACE_FRONT, 'tenant': None,
'platform': get_id(Platform, 'juniper-junos'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'position': 41,
'rack': '1',
'manufacturer': get_id(Manufacturer, 'juniper'), 'manufacturer': get_id(Manufacturer, 'juniper'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_FRONT,
'position': 41,
'platform': get_id(Platform, 'juniper-junos'),
'status': STATUS_ACTIVE,
}) })
self.assertTrue(test.is_valid(), test.fields['position'].choices) self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save()) self.assertTrue(test.save())
def test_racked_device_occupied(self): def test_racked_device_occupied(self):
test = DeviceForm(data={ test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'leaf-switch'),
'name': 'test', 'name': 'test',
'site': get_id(Site, 'test1'), 'device_role': get_id(DeviceRole, 'leaf-switch'),
'face': RACK_FACE_FRONT, 'tenant': None,
'platform': get_id(Platform, 'juniper-junos'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'position': 1,
'rack': '1',
'manufacturer': get_id(Manufacturer, 'juniper'), 'manufacturer': get_id(Manufacturer, 'juniper'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_FRONT,
'position': 1,
'platform': get_id(Platform, 'juniper-junos'),
'status': STATUS_ACTIVE,
}) })
self.assertFalse(test.is_valid()) self.assertFalse(test.is_valid())
def test_non_racked_device(self): def test_non_racked_device(self):
test = DeviceForm(data={ test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'pdu'),
'name': 'test', 'name': 'test',
'site': get_id(Site, 'test1'), 'device_role': get_id(DeviceRole, 'pdu'),
'face': None, 'tenant': None,
'platform': None,
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'position': None,
'rack': '1',
'manufacturer': get_id(Manufacturer, 'servertech'), 'manufacturer': get_id(Manufacturer, 'servertech'),
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': None,
'position': None,
'platform': None,
'status': STATUS_ACTIVE,
}) })
self.assertTrue(test.is_valid()) self.assertTrue(test.is_valid())
self.assertTrue(test.save()) self.assertTrue(test.save())
def test_non_racked_device_with_face(self): def test_non_racked_device_with_face(self):
test = DeviceForm(data={ test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'pdu'),
'name': 'test', 'name': 'test',
'site': get_id(Site, 'test1'), 'device_role': get_id(DeviceRole, 'pdu'),
'face': RACK_FACE_REAR, 'tenant': None,
'platform': None,
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'position': None,
'rack': '1',
'manufacturer': get_id(Manufacturer, 'servertech'), 'manufacturer': get_id(Manufacturer, 'servertech'),
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_REAR,
'position': None,
'platform': None,
'status': STATUS_ACTIVE,
}) })
self.assertTrue(test.is_valid()) self.assertTrue(test.is_valid())
self.assertTrue(test.save()) self.assertTrue(test.save())

View File

@ -26,6 +26,12 @@ urlpatterns = [
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
# Racks # Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'), url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),

View File

@ -8,6 +8,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Count, Sum from django.db.models import Count, Sum
from django.db.models.functions import Coalesce
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode from django.utils.http import urlencode
@ -26,7 +27,7 @@ from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
Site, RackRole, Site,
) )
@ -137,7 +138,7 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
# #
class RackGroupListView(ObjectListView): class RackGroupListView(ObjectListView):
queryset = RackGroup.objects.annotate(rack_count=Count('racks')) queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
filter = filters.RackGroupFilter filter = filters.RackGroupFilter
filter_form = forms.RackGroupFilterForm filter_form = forms.RackGroupFilterForm
table = tables.RackGroupTable table = tables.RackGroupTable
@ -149,6 +150,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackgroup' permission_required = 'dcim.change_rackgroup'
model = RackGroup model = RackGroup
form_class = forms.RackGroupForm form_class = forms.RackGroupForm
success_url = 'dcim:rackgroup_list'
cancel_url = 'dcim:rackgroup_list' cancel_url = 'dcim:rackgroup_list'
@ -158,13 +160,39 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
default_redirect_url = 'dcim:rackgroup_list' default_redirect_url = 'dcim:rackgroup_list'
#
# Rack roles
#
class RackRoleListView(ObjectListView):
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable
edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole']
template_name = 'dcim/rackrole_list.html'
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackrole'
model = RackRole
form_class = forms.RackRoleForm
success_url = 'dcim:rackrole_list'
cancel_url = 'dcim:rackrole_list'
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole'
cls = RackRole
default_redirect_url = 'dcim:rackrole_list'
# #
# Racks # Racks
# #
class RackListView(ObjectListView): class RackListView(ObjectListView):
queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type')\ queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
.annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height')) .annotate(device_count=Count('devices', distinct=True),
u_consumed=Coalesce(Sum('devices__device_type__u_height'), 0))
filter = filters.RackFilter filter = filters.RackFilter
filter_form = forms.RackFilterForm filter_form = forms.RackFilterForm
table = tables.RackTable table = tables.RackTable
@ -223,11 +251,12 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form): def update_objects(self, pk_list, form):
fields_to_update = {} fields_to_update = {}
if form.cleaned_data['tenant'] == 0: for field in ['group', 'tenant', 'role']:
fields_to_update['tenant'] = None if form.cleaned_data[field] == 0:
elif form.cleaned_data['tenant']: fields_to_update[field] = None
fields_to_update['tenant'] = form.cleaned_data['tenant'] elif form.cleaned_data[field]:
for field in ['site', 'group', 'tenant', 'u_height', 'comments']: fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'type', 'width', 'u_height', 'comments']:
if form.cleaned_data[field]: if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field] fields_to_update[field] = form.cleaned_data[field]
@ -533,8 +562,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class DeviceListView(ObjectListView): class DeviceListView(ObjectListView):
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip4', queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'rack__site',
'primary_ip6') 'primary_ip4', 'primary_ip6')
filter = filters.DeviceFilter filter = filters.DeviceFilter
filter_form = forms.DeviceFilterForm filter_form = forms.DeviceFilterForm
table = tables.DeviceTable table = tables.DeviceTable
@ -680,7 +709,8 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
def device_inventory(request, pk): def device_inventory(request, pk):
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device, pk=pk)
modules = Module.objects.filter(device=device, parent=None).prefetch_related('submodules') modules = Module.objects.filter(device=device, parent=None).select_related('manufacturer')\
.prefetch_related('submodules')
return render(request, 'dcim/device_inventory.html', { return render(request, 'dcim/device_inventory.html', {
'device': device, 'device': device,

View File

@ -182,18 +182,14 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
self.fields['vlan'].choices = [] self.fields['vlan'].choices = []
def clean_prefix(self): def clean_prefix(self):
data = self.cleaned_data['prefix'] prefix = self.cleaned_data['prefix']
try:
prefix = IPNetwork(data)
except:
raise
if prefix.version == 4 and prefix.prefixlen == 32: if prefix.version == 4 and prefix.prefixlen == 32:
raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 " raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
"addresses instead.") "addresses instead.")
elif prefix.version == 6 and prefix.prefixlen == 128: elif prefix.version == 6 and prefix.prefixlen == 128:
raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 " raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
"addresses instead.") "addresses instead.")
return data return prefix
class PrefixFromCSVForm(forms.ModelForm): class PrefixFromCSVForm(forms.ModelForm):

View File

@ -254,12 +254,13 @@ class Prefix(CreatedUpdatedModel):
def clean(self): def clean(self):
# Disallow host masks # Disallow host masks
if self.prefix.version == 4 and self.prefix.prefixlen == 32: if self.prefix:
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses " if self.prefix.version == 4 and self.prefix.prefixlen == 32:
"instead.") raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
elif self.prefix.version == 6 and self.prefix.prefixlen == 128: "instead.")
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses " elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
"instead.") raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
"instead.")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.prefix: if self.prefix:

View File

@ -43,12 +43,22 @@ IPADDRESS_LINK = """
{% if record.pk %} {% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a> <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %} {% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a> <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
{% else %} {% else %}
{{ record.0 }} {{ record.0 }}
{% endif %} {% endif %}
""" """
VRF_LINK = """
{% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
{% elif prefix.vrf %}
{{ prefix.vrf }}
{% else %}
Global
{% endif %}
"""
STATUS_LABEL = """ STATUS_LABEL = """
{% if record.pk %} {% if record.pk %}
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span> <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
@ -149,7 +159,7 @@ class PrefixTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix') prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
role = tables.Column(verbose_name='Role') role = tables.Column(verbose_name='Role')
@ -183,7 +193,7 @@ class PrefixBriefTable(BaseTable):
class IPAddressTable(BaseTable): class IPAddressTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
verbose_name='Device') verbose_name='Device')

View File

@ -305,7 +305,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class PrefixListView(ObjectListView): class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role') queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm filter_form = forms.PrefixFilterForm
table = tables.PrefixTable table = tables.PrefixTable
@ -445,7 +445,7 @@ def prefix_ipaddresses(request, pk):
# #
class IPAddressListView(ObjectListView): class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device') queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable table = tables.IPAddressTable
@ -550,7 +550,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class VLANGroupListView(ObjectListView): class VLANGroupListView(ObjectListView):
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans')) queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable table = tables.VLANGroupTable
@ -562,6 +562,7 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup' permission_required = 'ipam.change_vlangroup'
model = VLANGroup model = VLANGroup
form_class = forms.VLANGroupForm form_class = forms.VLANGroupForm
success_url = 'ipam:vlangroup_list'
cancel_url = 'ipam:vlangroup_list' cancel_url = 'ipam:vlangroup_list'
@ -576,7 +577,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class VLANListView(ObjectListView): class VLANListView(ObjectListView):
queryset = VLAN.objects.select_related('site', 'role') queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter filter = filters.VLANFilter
filter_form = forms.VLANFilterForm filter_form = forms.VLANFilterForm
table = tables.VLANTable table = tables.VLANTable

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.4.2' VERSION = '1.5.0'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@ -172,7 +172,6 @@ MESSAGE_TAGS = {
# Authentication URLs # Authentication URLs
LOGIN_URL = '/login/' LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGOUT_URL = '/logout/'
# Secrets # Secrets
SECRETS_MIN_PUBKEY_SIZE = 2048 SECRETS_MIN_PUBKEY_SIZE = 2048

View File

@ -21,6 +21,9 @@ body {
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */ margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
padding-bottom: 30px; padding-bottom: 30px;
} }
.navbar-brand {
padding: 12px 15px 8px;
}
.footer, .push { .footer, .push {
height: 60px; /* .push must be the same height as .footer */ height: 60px; /* .push must be the same height as .footer */
} }
@ -291,27 +294,39 @@ ul.rack_near_face li.empty:hover a {
display: block; display: block;
} }
/* Rack elevation colors (from http://flatuicolors.com) */ /* Colors (from http://flatuicolors.com) */
.teal { background-color: #1abc9c; border-bottom: 1px solid #16a085; } .teal { background-color: #1abc9c; }
.teal:hover { background-color: #16a085; } .green { background-color: #2ecc71; }
.green { background-color: #2ecc71; border-bottom: 1px solid #27ae60; } .blue { background-color: #3498db; }
.green:hover { background-color: #27ae60; } .purple { background-color: #9b59b6; }
.blue { background-color: #3498db; border-bottom: 1px solid #2980b9; } .yellow { background-color: #f1c40f; }
.blue:hover { background-color: #2980b9; } .orange { background-color: #e67e22; }
.purple { background-color: #9b59b6; border-bottom: 1px solid #8e44ad; } .red { background-color: #e74c3c; }
.purple:hover { background-color: #8e44ad; } .light_gray { background-color: #dce2e3; }
.yellow { background-color: #f1c40f; border-bottom: 1px solid #f39c12; } .medium_gray { background-color: #95a5a6; }
.yellow:hover { background-color: #f39c12; } .dark_gray { background-color: #34495e; }
.orange { background-color: #e67e22; border-bottom: 1px solid #d35400; }
.orange:hover { background-color: #d35400; } /* Rack elevation coloring */
.red { background-color: #e74c3c; border-bottom: 1px solid #c0392b; } ul.rack .teal { border-bottom: 1px solid #16a085; }
.red:hover { background-color: #c0392b; } ul.rack .teal:hover { background-color: #16a085; }
.light_gray { background-color: #dce2e3; border-bottom: 1px solid #bdc3c7; } ul.rack .green { border-bottom: 1px solid #27ae60; }
.light_gray:hover { background-color: #bdc3c7; } ul.rack .green:hover { background-color: #27ae60; }
.medium_gray { background-color: #95a5a6; border-bottom: 1px solid #7f8c8d; } ul.rack .blue { border-bottom: 1px solid #2980b9; }
.medium_gray:hover { background-color: #7f8c8d; } ul.rack .blue:hover { background-color: #2980b9; }
.dark_gray { background-color: #34495e; border-bottom: 1px solid #2c3e50; } ul.rack .purple { border-bottom: 1px solid #8e44ad; }
.dark_gray:hover { background-color: #2c3e50; } ul.rack .purple:hover { background-color: #8e44ad; }
ul.rack .yellow { border-bottom: 1px solid #f39c12; }
ul.rack .yellow:hover { background-color: #f39c12; }
ul.rack .orange { border-bottom: 1px solid #d35400; }
ul.rack .orange:hover { background-color: #d35400; }
ul.rack .red { border-bottom: 1px solid #c0392b; }
ul.rack .red:hover { background-color: #c0392b; }
ul.rack .light_gray { border-bottom: 1px solid #bdc3c7; }
ul.rack .light_gray:hover { background-color: #bdc3c7; }
ul.rack .medium_gray { border-bottom: 1px solid #7f8c8d; }
ul.rack .medium_gray:hover { background-color: #7f8c8d; }
ul.rack .dark_gray { border-bottom: 1px solid #2c3e50; }
ul.rack .dark_gray:hover { background-color: #2c3e50; }
/* Misc */ /* Misc */
.banner-bottom { .banner-bottom {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -8,6 +8,7 @@
<link rel="stylesheet" href="{% static 'font-awesome-4.6.3/css/font-awesome.min.css' %}"> <link rel="stylesheet" href="{% static 'font-awesome-4.6.3/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}"> <link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}"> <link rel="stylesheet" href="{% static 'css/base.css' %}">
<link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
</head> </head>
<body> <body>
<nav class="navbar navbar-default navbar-fixed-top"> <nav class="navbar navbar-default navbar-fixed-top">
@ -19,7 +20,9 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="/">NetBox</a> <a class="navbar-brand" href="/">
<img src="{% static 'img/netbox_logo.png' %}" />
</a>
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %} {% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
@ -58,6 +61,11 @@
{% if perms.dcim.add_rackgroup %} {% if perms.dcim.add_rackgroup %}
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li> <li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
{% endif %} {% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
{% if perms.dcim.add_rackrole %}
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
{% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}"> <li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">

View File

@ -82,12 +82,13 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Port Speed</td> <td>Speed</td>
<td> <td>
{% if circuit.port_speed %} {% if circuit.upstream_speed %}
{{ circuit.port_speed_human }} <i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
{% else %} {% else %}
<span class="text-muted">N/A</span> {{ circuit.port_speed_human }}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -19,6 +19,7 @@
<div class="panel-heading"><strong>Bandwidth</strong></div> <div class="panel-heading"><strong>Bandwidth</strong></div>
<div class="panel-body"> <div class="panel-body">
{% render_field form.port_speed %} {% render_field form.port_speed %}
{% render_field form.upstream_speed %}
{% render_field form.commit_rate %} {% render_field form.commit_rate %}
</div> </div>
</div> </div>

View File

@ -60,8 +60,13 @@
</tr> </tr>
<tr> <tr>
<td>Port Speed</td> <td>Port Speed</td>
<td>Physical speed in Kbps/td> <td>Physical speed in Kbps</td>
<td>10000</td> <td>100000</td>
</tr>
<tr>
<td>Upstream Speed</td>
<td>Upstream speed in Kbps (optional)</td>
<td>20000</td>
</tr> </tr>
<tr> <tr>
<td>Commit rate</td> <td>Commit rate</td>
@ -81,7 +86,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre> <pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,100000,,2000,937649,PP8371 ports 13/14</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -36,6 +36,11 @@
<td>Functional role of device</td> <td>Functional role of device</td>
<td>Blade Server</td> <td>Blade Server</td>
</tr> </tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr> <tr>
<td>Device manufacturer</td> <td>Device manufacturer</td>
<td>Hardware manufacturer</td> <td>Hardware manufacturer</td>
@ -69,7 +74,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre> <pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -32,6 +32,7 @@
<tr> <tr>
<th>Module</th> <th>Module</th>
<th></th> <th></th>
<th>Manufacturer</th>
<th>Part Number</th> <th>Part Number</th>
<th>Serial Number</th> <th>Serial Number</th>
<th></th> <th></th>
@ -42,6 +43,7 @@
<tr> <tr>
<td>{{ m.name }}</td> <td>{{ m.name }}</td>
<td>{% if not m.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not m.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m.manufacturer|default:'' }}</td>
<td>{{ m.part_id }}</td> <td>{{ m.part_id }}</td>
<td>{{ m.serial }}</td> <td>{{ m.serial }}</td>
<td class="text-right"> <td class="text-right">
@ -57,6 +59,7 @@
<tr> <tr>
<td style="padding-left: 20px">{{ m2.name }}</td> <td style="padding-left: 20px">{{ m2.name }}</td>
<td>{% if not m2.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not m2.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m2.manufacturer|default:'' }}</td>
<td>{{ m2.part_id }}</td> <td>{{ m2.part_id }}</td>
<td>{{ m2.serial }}</td> <td>{{ m2.serial }}</td>
<td class="text-right"> <td class="text-right">
@ -72,6 +75,7 @@
<tr> <tr>
<td style="padding-left: 40px">{{ m3.name }}</td> <td style="padding-left: 40px">{{ m3.name }}</td>
<td>{% if not m3.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not m3.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m3.manufacturer|default:'' }}</td>
<td>{{ m3.part_id }}</td> <td>{{ m3.part_id }}</td>
<td>{{ m3.serial }}</td> <td>{{ m3.serial }}</td>
<td class="text-right"> <td class="text-right">
@ -87,6 +91,7 @@
<tr> <tr>
<td style="padding-left: 60px">{{ m4.name }}</td> <td style="padding-left: 60px">{{ m4.name }}</td>
<td>{% if not m4.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not m4.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m4.manufacturer|default:'' }}</td>
<td>{{ m4.part_id }}</td> <td>{{ m4.part_id }}</td>
<td>{{ m4.serial }}</td> <td>{{ m4.serial }}</td>
<td class="text-right"> <td class="text-right">

View File

@ -96,6 +96,20 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Type</td>
<td>
{% if rack.type %}
{{ rack.get_type_display }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Width</td>
<td>{{ rack.get_width_display }}</td>
</tr>
<tr> <tr>
<td>Height</td> <td>Height</td>
<td>{{ rack.u_height }}U</td> <td>{{ rack.u_height }}U</td>

View File

@ -4,13 +4,24 @@
{% block title %}Rack Bulk Edit{% endblock %} {% block title %}Rack Bulk Edit{% endblock %}
{% block select_objects_table %} {% block select_objects_table %}
<tr>
<th>Name</th>
<th>Site</th>
<th>Group</th>
<th>Tenant</th>
<th>Type</th>
<th>Width</th>
<th>Height</th>
</tr>
{% for rack in selected_objects %} {% for rack in selected_objects %}
<tr> <tr>
<td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td> <td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
<td>{{ rack.facility_id }}</td>
<td>{{ rack.site }}</td> <td>{{ rack.site }}</td>
<td>{{ rack.group }}</td>
<td>{{ rack.tenant }}</td> <td>{{ rack.tenant }}</td>
<td>{{ rack.u_height }}</td> <td>{{ rack.get_type_display }}</td>
<td>{{ rack.get_width_display }}</td>
<td>{{ rack.u_height }}U</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -10,6 +10,8 @@
{% render_field form.name %} {% render_field form.name %}
{% render_field form.facility_id %} {% render_field form.facility_id %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.type %}
{% render_field form.width %}
{% render_field form.u_height %} {% render_field form.u_height %}
</div> </div>
</div> </div>

View File

@ -53,6 +53,16 @@
<td>Name of tenant (optional)</td> <td>Name of tenant (optional)</td>
<td>Pied Piper</td> <td>Pied Piper</td>
</tr> </tr>
<tr>
<td>Type</td>
<td>Rack type (optional)</td>
<td>4-post cabinet</td>
</tr>
<tr>
<td>Width</td>
<td>Rail-to-rail width (19 or 23 inches)</td>
<td>19</td>
</tr>
<tr> <tr>
<td>Height</td> <td>Height</td>
<td>Height in rack units</td> <td>Height in rack units</td>
@ -61,7 +71,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,42</pre> <pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,4-post cabinet,19,42</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,21 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Rack Role{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackrole %}
<a href="{% url 'dcim:rackrole_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack role
</a>
{% endif %}
</div>
<h1>Rack Roles</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -27,6 +27,13 @@ def expand_pattern(string):
yield "{}{}{}".format(lead, i, remnant) yield "{}{}{}".format(lead, i, remnant)
def add_blank_choice(choices):
"""
Add a blank choice to the beginning of a choices list.
"""
return ((None, '---------'),) + choices
# #
# Widgets # Widgets
# #

View File

@ -28,3 +28,10 @@ class ToggleColumn(tables.CheckBoxColumn):
@property @property
def header(self): def header(self):
return mark_safe('<input type="checkbox" name="_all" title="Select all" />') return mark_safe('<input type="checkbox" name="_all" title="Select all" />')
class ColorColumn(tables.Column):
def render(self, record):
html = '<label class="label {}">{}</label>'.format(record.color, record.get_color_display())
return mark_safe(html)

View File

@ -1,13 +1,14 @@
cryptography==1.4 cryptography==1.4
Django==1.9.8 Django==1.10
django-debug-toolbar==1.4 django-debug-toolbar==1.4
django-filter==0.13.0 django-filter==0.13.0
django-rest-swagger==0.3.7 django-rest-swagger==0.3.10
django-tables2==1.2.1 django-tables2==1.2.1
djangorestframework==3.3.3 djangorestframework==3.4.3
graphviz==0.4.10 graphviz==0.4.10
Markdown==2.6.6 Markdown==2.6.6
ncclient==0.4.7 natsort>=5.0.0
ncclient==0.5.2
netaddr==0.7.18 netaddr==0.7.18
paramiko==2.0.0 paramiko==2.0.0
psycopg2==2.6.1 psycopg2==2.6.1
@ -15,4 +16,3 @@ py-gfm==0.1.3
pycrypto==2.6.1 pycrypto==2.6.1
sqlparse==0.1.19 sqlparse==0.1.19
xmltodict==0.10.2 xmltodict==0.10.2
natsort>=5.0.0