Initial work on regions

This commit is contained in:
Jeremy Stretch 2017-02-28 12:11:43 -05:00
parent 5520144ff4
commit f3b9930dea
26 changed files with 379 additions and 43 deletions

View File

@ -6,6 +6,10 @@ How you define sites will depend on the nature of your organization, but typical
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment. Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.
### Regions
Sites can be arranged by geographic region. A region might represent a continent, country, city, campus, or other area depending on your use case. Region assignment is optional.
--- ---
# Racks # Racks

View File

@ -119,9 +119,17 @@ class CircuitListView(ObjectListView):
def circuit(request, pk): def circuit(request, pk):
circuit = get_object_or_404(Circuit, pk=pk) circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first() termination_a = CircuitTermination.objects.select_related(
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first() 'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()
return render(request, 'circuits/circuit.html', { return render(request, 'circuits/circuit.html', {
'circuit': circuit, 'circuit': circuit,

View File

@ -4,10 +4,19 @@ 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, RackReservation, RackRole, Site, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
Site,
) )
@admin.register(Region)
class RegionAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(Site) @admin.register(Site)
class SiteAdmin(admin.ModelAdmin): class SiteAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'facility', 'asn'] list_display = ['name', 'slug', 'facility', 'asn']

View File

@ -5,22 +5,40 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_FRONT, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_FRONT,
RACK_FACE_REAR, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, RACK_FACE_REAR, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
) )
from extras.api.serializers import CustomFieldSerializer from extras.api.serializers import CustomFieldSerializer
from tenancy.api.serializers import TenantNestedSerializer from tenancy.api.serializers import TenantNestedSerializer
#
# Regions
#
class RegionSerializer(serializers.ModelSerializer):
class Meta:
model = RackGroup
fields = ['id', 'name', 'slug']
class RegionNestedSerializer(RegionSerializer):
class Meta(RegionSerializer.Meta):
pass
# #
# Sites # Sites
# #
class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer): class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
region = RegionNestedSerializer()
tenant = TenantNestedSerializer() tenant = TenantNestedSerializer()
class Meta: class Meta:
model = Site model = Site
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', fields = ['id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
'count_vlans', 'count_racks', 'count_devices', 'count_circuits'] 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']

View File

@ -8,6 +8,10 @@ from .views import *
urlpatterns = [ urlpatterns = [
# Regions
url(r'^regions/$', RegionListView.as_view(), name='region_list'),
url(r'^regions/(?P<pk>\d+)/$', RegionDetailView.as_view(), name='region_detail'),
# Sites # Sites
url(r'^sites/$', SiteListView.as_view(), name='site_list'), url(r'^sites/$', SiteListView.as_view(), name='site_list'),
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'), url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),

View File

@ -11,7 +11,7 @@ from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection,
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, VIRTUAL_IFACE_TYPES,
) )
from dcim import filters from dcim import filters
@ -22,6 +22,26 @@ from .exceptions import MissingFilterException
from . import serializers from . import serializers
#
# Regions
#
class RegionListView(generics.ListAPIView):
"""
List all regions
"""
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
class RegionDetailView(generics.RetrieveAPIView):
"""
Retrieve a single region
"""
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
# #
# Sites # Sites
# #

View File

@ -8,7 +8,7 @@ from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter from utilities.filters import NullableModelMultipleChoiceFilter
from .models import ( from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, VIRTUAL_IFACE_TYPES,
) )
@ -18,6 +18,17 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search', action='search',
label='Search', label='Search',
) )
region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),

View File

@ -20,7 +20,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
VIRTUAL_IFACE_TYPES VIRTUAL_IFACE_TYPES
) )
@ -63,6 +63,18 @@ class DeviceComponentForm(BootstrapMixin, forms.Form):
super(DeviceComponentForm, self).__init__(*args, **kwargs) super(DeviceComponentForm, self).__init__(*args, **kwargs)
#
# Regions
#
class RegionForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = Region
fields = ['name', 'slug']
# #
# Sites # Sites
# #
@ -73,8 +85,10 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class Meta: class Meta:
model = Site model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', fields = [
'contact_phone', 'contact_email', 'comments'] 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = { widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}), 'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}), 'shipping_address': SmallTextarea(attrs={'rows': 3}),
@ -89,12 +103,22 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class SiteFromCSVForm(forms.ModelForm): class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, region = forms.ModelChoiceField(
error_messages={'invalid_choice': 'Tenant not found.'}) Region.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
class Meta: class Meta:
model = Site model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class SiteImportForm(BootstrapMixin, BulkImportForm): class SiteImportForm(BootstrapMixin, BulkImportForm):
@ -103,18 +127,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
region = forms.ModelChoiceField(queryset=Region.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN') asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
class Meta: class Meta:
nullable_fields = ['tenant', 'asn'] nullable_fields = ['region', 'tenant', 'asn']
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site model = Site
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug', region = FilterChoiceField(
null_option=(0, 'None')) queryset=Region.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
)
# #

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-28 14:48
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0030_interface_add_lag'),
]
operations = [
migrations.CreateModel(
name='Region',
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)),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='site',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
),
]

View File

@ -200,6 +200,28 @@ RPC_CLIENT_CHOICES = [
] ]
#
# Regions
#
@python_2_unicode_compatible
class Region(models.Model):
"""
Sites can be grouped within geographic Regions.
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
# #
# Sites # Sites
# #
@ -218,7 +240,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
""" """
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)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT) region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
facility = models.CharField(max_length=50, blank=True) facility = models.CharField(max_length=50, blank=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN') asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True) physical_address = models.CharField(max_length=200, blank=True)

View File

@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, Site, RackGroup, Region, Site,
) )
@ -20,6 +20,12 @@ DEVICE_LINK = """
</a> </a>
""" """
REGION_ACTIONS = """
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKGROUP_ACTIONS = """ RACKGROUP_ACTIONS = """
{% if perms.dcim.change_rackgroup %} {% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@ -76,6 +82,26 @@ UTILIZATION_GRAPH = """
""" """
#
# Regions
#
class RegionTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
site_count = tables.Column(verbose_name='Sites')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = Region
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
# #
# Sites # Sites
# #
@ -84,6 +110,7 @@ class SiteTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name') name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility') facility = tables.Column(verbose_name='Facility')
region = tables.LinkColumn(verbose_name='Region')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN') asn = tables.Column(verbose_name='ASN')
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks') rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
@ -94,8 +121,10 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Site model = Site
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', fields = (
'vlan_count', 'circuit_count') 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count',
)
# #

View File

@ -17,6 +17,7 @@ class SiteTest(APITestCase):
'id', 'id',
'name', 'name',
'slug', 'slug',
'region',
'tenant', 'tenant',
'facility', 'facility',
'asn', 'asn',

View File

@ -8,6 +8,12 @@ from . import views
urlpatterns = [ urlpatterns = [
# Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
# Sites # Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'), url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),

View File

@ -26,7 +26,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,
RackReservation, RackRole, Site, RackReservation, RackRole, Region, Site,
) )
@ -129,12 +129,37 @@ class ComponentDeleteView(ObjectDeleteView):
return obj.device.get_absolute_url() return obj.device.get_absolute_url()
#
# Regions
#
class RegionListView(ObjectListView):
queryset = Region.objects.annotate(site_count=Count('sites'))
table = tables.RegionTable
template_name = 'dcim/region_list.html'
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_region'
model = Region
form_class = forms.RegionForm
def get_return_url(self, obj):
return reverse('dcim:region_list')
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
cls = Region
default_return_url = 'dcim:region_list'
# #
# Sites # Sites
# #
class SiteListView(ObjectListView): class SiteListView(ObjectListView):
queryset = Site.objects.select_related('tenant') queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter filter = filters.SiteFilter
filter_form = forms.SiteFilterForm filter_form = forms.SiteFilterForm
table = tables.SiteTable table = tables.SiteTable
@ -143,7 +168,7 @@ class SiteListView(ObjectListView):
def site(request, slug): def site(request, slug):
site = get_object_or_404(Site, slug=slug) site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
stats = { stats = {
'rack_count': Rack.objects.filter(site=site).count(), 'rack_count': Rack.objects.filter(site=site).count(),
'device_count': Device.objects.filter(rack__site=site).count(), 'device_count': Device.objects.filter(rack__site=site).count(),
@ -263,7 +288,7 @@ class RackListView(ObjectListView):
def rack(request, pk): def rack(request, pk):
rack = get_object_or_404(Rack, pk=pk) rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\ nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
.select_related('device_type__manufacturer') .select_related('device_type__manufacturer')
@ -638,7 +663,9 @@ class DeviceListView(ObjectListView):
def device(request, pk): def device(request, pk):
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device.objects.select_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
), pk=pk)
console_ports = natsorted( console_ports = natsorted(
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
) )

View File

@ -393,7 +393,9 @@ class PrefixListView(ObjectListView):
def prefix(request, pk): def prefix(request, pk):
prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'role'), pk=pk) prefix = get_object_or_404(Prefix.objects.select_related(
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
), pk=pk)
try: try:
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix)) aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
@ -731,7 +733,7 @@ class VLANListView(ObjectListView):
def vlan(request, pk): def vlan(request, pk):
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk) vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes)) prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',) prefix_table.exclude = ('vlan',)

View File

@ -37,6 +37,11 @@
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li> <li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
{% if perms.dcim.add_region %}
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li> <li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
{% if perms.tenancy.add_tenant %} {% if perms.tenancy.add_tenant %}
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li> <li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>

View File

@ -66,6 +66,10 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if circuit.tenant %} {% if circuit.tenant %}
{% if circuit.tenant.group %}
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a> <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>

View File

@ -27,6 +27,10 @@
<tr> <tr>
<td>Site</td> <td>Site</td>
<td> <td>
{% if termination.site.region %}
<a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a> <a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
</td> </td>
</tr> </tr>
@ -34,7 +38,8 @@
<td>Termination</td> <td>Termination</td>
<td> <td>
{% if termination.interface %} {% if termination.interface %}
<span><a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> {{ termination.interface }}</span> <a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a>
<i class="fa fa-angle-right"></i> {{ termination.interface }}
{% else %} {% else %}
<span class="text-muted">Not defined</span> <span class="text-muted">Not defined</span>
{% endif %} {% endif %}

View File

@ -14,19 +14,13 @@
<strong>Device</strong> <strong>Device</strong>
</div> </div>
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Site</td> <td>Site</td>
<td> <td>
{% if device.site.region %}
<a href="{{ device.site.region.get_absolute_url }}">{{ device.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a> <a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a>
</td> </td>
</tr> </tr>
@ -34,7 +28,11 @@
<td>Rack</td> <td>Rack</td>
<td> <td>
{% if device.rack %} {% if device.rack %}
<span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span> {% if device.rack.group %}
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@ -57,6 +55,20 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
{% if device.tenant.group %}
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Device Type</td> <td>Device Type</td>
<td> <td>

View File

@ -64,6 +64,10 @@
<tr> <tr>
<td>Site</td> <td>Site</td>
<td> <td>
{% if rack.site.region %}
<a href="{{ rack.site.region.get_absolute_url }}">{{ rack.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a> <a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a>
</td> </td>
</tr> </tr>
@ -91,6 +95,10 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if rack.tenant %} {% if rack.tenant %}
{% if rack.tenant.group %}
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a> <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>

View File

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

View File

@ -10,6 +10,9 @@
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li> <li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
{% if site.region %}
<li> <a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
{% endif %}
<li>{{ site }}</li> <li>{{ site }}</li>
</ol> </ol>
</div> </div>
@ -55,10 +58,24 @@
<strong>Site</strong> <strong>Site</strong>
</div> </div>
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Region</td>
<td>
{% if site.region %}
<a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if site.tenant %} {% if site.tenant %}
{% if site.tenant.group %}
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a> <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -85,6 +102,13 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Contact Info</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr> <tr>
<td>Physical Address</td> <td>Physical Address</td>
<td> <td>

View File

@ -7,6 +7,7 @@
<div class="panel-body"> <div class="panel-body">
{% render_field form.name %} {% render_field form.name %}
{% render_field form.slug %} {% render_field form.slug %}
{% render_field form.region %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.facility %} {% render_field form.facility %}
{% render_field form.asn %} {% render_field form.asn %}

View File

@ -38,6 +38,11 @@
<td>URL-friendly name</td> <td>URL-friendly name</td>
<td>ash4-south</td> <td>ash4-south</td>
</tr> </tr>
<tr>
<td>Region</td>
<td>Name of region (optional)</td>
<td>North America</td>
</tr>
<tr> <tr>
<td>Tenant</td> <td>Tenant</td>
<td>Name of tenant (optional)</td> <td>Name of tenant (optional)</td>
@ -71,7 +76,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre> <pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -30,8 +30,16 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if prefix.tenant %} {% if prefix.tenant %}
{% if prefix.tenant.group %}
<a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a> <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
{% elif prefix.vrf.tenant %} {% elif prefix.vrf.tenant %}
{% if prefix.vrf.tenant.group %}
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a> <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
<label class="label label-info">Inherited</label> <label class="label label-info">Inherited</label>
{% else %} {% else %}
@ -53,6 +61,10 @@
<td>Site</td> <td>Site</td>
<td> <td>
{% if prefix.site %} {% if prefix.site %}
{% if prefix.site.region %}
<a href="{{ prefix.site.region.get_absolute_url }}">{{ prefix.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a> <a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -63,6 +75,10 @@
<td>VLAN</td> <td>VLAN</td>
<td> <td>
{% if prefix.vlan %} {% if prefix.vlan %}
{% if prefix.vlan.group %}
<a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a> <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -79,7 +95,7 @@
<td>Role</td> <td>Role</td>
<td> <td>
{% if prefix.role %} {% if prefix.role %}
<span>{{ prefix.role }}</span> <a href="{% url 'ipam:prefix_list' %}?role={{ prefix.role.slug }}">{{ prefix.role }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}

View File

@ -57,6 +57,10 @@
<td>Site</td> <td>Site</td>
<td> <td>
{% if vlan.site %} {% if vlan.site %}
{% if vlan.site.region %}
<a href="{{ vlan.site.region.get_absolute_url }}">{{ vlan.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a> <a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -85,6 +89,10 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if vlan.tenant %} {% if vlan.tenant %}
{% if vlan.tenant.group %}
<a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a> <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -101,7 +109,7 @@
<td>Role</td> <td>Role</td>
<td> <td>
{% if vlan.role %} {% if vlan.role %}
<span>{{ vlan.role }}</span> <a href="{% url 'ipam:vlan_list' %}?role={{ vlan.role.slug }}">{{ vlan.role }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}