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.
### 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

View File

@ -119,9 +119,17 @@ class CircuitListView(ObjectListView):
def circuit(request, pk):
circuit = get_object_or_404(Circuit, pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related(
'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', {
'circuit': circuit,

View File

@ -4,10 +4,19 @@ from django.db.models import Count
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
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)
class SiteAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'facility', 'asn']

View File

@ -5,22 +5,40 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
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 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
#
class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
region = RegionNestedSerializer()
tenant = TenantNestedSerializer()
class Meta:
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',
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']

View File

@ -8,6 +8,10 @@ from .views import *
urlpatterns = [
# Regions
url(r'^regions/$', RegionListView.as_view(), name='region_list'),
url(r'^regions/(?P<pk>\d+)/$', RegionDetailView.as_view(), name='region_detail'),
# Sites
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
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 (
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,
)
from dcim import filters
@ -22,6 +22,26 @@ from .exceptions import MissingFilterException
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
#

View File

@ -8,7 +8,7 @@ from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter
from .models import (
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,
)
@ -18,6 +18,17 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='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(
name='tenant',
queryset=Tenant.objects.all(),

View File

@ -20,7 +20,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
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
)
@ -63,6 +63,18 @@ class DeviceComponentForm(BootstrapMixin, forms.Form):
super(DeviceComponentForm, self).__init__(*args, **kwargs)
#
# Regions
#
class RegionForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = Region
fields = ['name', 'slug']
#
# Sites
#
@ -73,8 +85,10 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class Meta:
model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
'contact_phone', 'contact_email', 'comments']
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}),
@ -89,12 +103,22 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
region = forms.ModelChoiceField(
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:
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):
@ -103,18 +127,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
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)
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
class Meta:
nullable_fields = ['tenant', 'asn']
nullable_fields = ['region', 'tenant', 'asn']
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
null_option=(0, 'None'))
region = FilterChoiceField(
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
#
@ -218,7 +240,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
"""
name = models.CharField(max_length=50, 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)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)

View File

@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, Site,
RackGroup, Region, Site,
)
@ -20,6 +20,12 @@ DEVICE_LINK = """
</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 = """
{% 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>
@ -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
#
@ -84,6 +110,7 @@ class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility')
region = tables.LinkColumn(verbose_name='Region')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN')
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
@ -94,8 +121,10 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count')
fields = (
'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',
'name',
'slug',
'region',
'tenant',
'facility',
'asn',

View File

@ -8,6 +8,12 @@ from . import views
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
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
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,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
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()
#
# 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
#
class SiteListView(ObjectListView):
queryset = Site.objects.select_related('tenant')
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
table = tables.SiteTable
@ -143,7 +168,7 @@ class SiteListView(ObjectListView):
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 = {
'rack_count': Rack.objects.filter(site=site).count(),
'device_count': Device.objects.filter(rack__site=site).count(),
@ -263,7 +288,7 @@ class RackListView(ObjectListView):
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)\
.select_related('device_type__manufacturer')
@ -638,7 +663,9 @@ class DeviceListView(ObjectListView):
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(
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):
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:
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
@ -731,7 +733,7 @@ class VLANListView(ObjectListView):
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')
prefix_table = tables.PrefixBriefTable(list(prefixes))
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>
{% endif %}
<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>
{% 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>

View File

@ -66,6 +66,10 @@
<td>Tenant</td>
<td>
{% 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>
{% else %}
<span class="text-muted">None</span>

View File

@ -27,6 +27,10 @@
<tr>
<td>Site</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>
</td>
</tr>
@ -34,7 +38,8 @@
<td>Termination</td>
<td>
{% 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 %}
<span class="text-muted">Not defined</span>
{% endif %}

View File

@ -14,19 +14,13 @@
<strong>Device</strong>
</div>
<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>
<td>Site</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>
</td>
</tr>
@ -34,7 +28,11 @@
<td>Rack</td>
<td>
{% 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 %}
<span class="text-muted">None</span>
{% endif %}
@ -57,6 +55,20 @@
{% endif %}
</td>
</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>
<td>Device Type</td>
<td>

View File

@ -64,6 +64,10 @@
<tr>
<td>Site</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>
</td>
</tr>
@ -91,6 +95,10 @@
<td>Tenant</td>
<td>
{% 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>
{% else %}
<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">
<ol class="breadcrumb">
<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>
</ol>
</div>
@ -55,10 +58,24 @@
<strong>Site</strong>
</div>
<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>
<td>Tenant</td>
<td>
{% 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>
{% else %}
<span class="text-muted">None</span>
@ -85,6 +102,13 @@
{% endif %}
</td>
</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>
<td>Physical Address</td>
<td>

View File

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

View File

@ -38,6 +38,11 @@
<td>URL-friendly name</td>
<td>ash4-south</td>
</tr>
<tr>
<td>Region</td>
<td>Name of region (optional)</td>
<td>North America</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
@ -71,7 +76,7 @@
</tbody>
</table>
<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>
{% endblock %}

View File

@ -30,8 +30,16 @@
<td>Tenant</td>
<td>
{% 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>
{% 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>
<label class="label label-info">Inherited</label>
{% else %}
@ -53,6 +61,10 @@
<td>Site</td>
<td>
{% 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>
{% else %}
<span class="text-muted">None</span>
@ -63,6 +75,10 @@
<td>VLAN</td>
<td>
{% 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>
{% else %}
<span class="text-muted">None</span>
@ -79,7 +95,7 @@
<td>Role</td>
<td>
{% if prefix.role %}
<span>{{ prefix.role }}</span>
<a href="{% url 'ipam:prefix_list' %}?role={{ prefix.role.slug }}">{{ prefix.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}

View File

@ -57,6 +57,10 @@
<td>Site</td>
<td>
{% 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>
{% else %}
<span class="text-muted">None</span>
@ -85,6 +89,10 @@
<td>Tenant</td>
<td>
{% 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>
{% else %}
<span class="text-muted">None</span>
@ -101,7 +109,7 @@
<td>Role</td>
<td>
{% if vlan.role %}
<span>{{ vlan.role }}</span>
<a href="{% url 'ipam:vlan_list' %}?role={{ vlan.role.slug }}">{{ vlan.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}