diff --git a/docs/data-model/dcim.md b/docs/data-model/dcim.md index a345312d5..7f83c685c 100644 --- a/docs/data-model/dcim.md +++ b/docs/data-model/dcim.md @@ -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 diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 466104883..1ffda899b 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -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, diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index fb4c281ac..147dc174b 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -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'] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index bad6202c8..928506bdc 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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'] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 432135925..0b9052d82 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -8,6 +8,10 @@ from .views import * urlpatterns = [ + # Regions + url(r'^regions/$', RegionListView.as_view(), name='region_list'), + url(r'^regions/(?P\d+)/$', RegionDetailView.as_view(), name='region_detail'), + # Sites url(r'^sites/$', SiteListView.as_view(), name='site_list'), url(r'^sites/(?P\d+)/$', SiteDetailView.as_view(), name='site_detail'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index f733e7e0d..042057c70 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -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 # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index eefea3bb2..3360d8149 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -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(), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index efd1860e3..eb96932d7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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') + ) # diff --git a/netbox/dcim/migrations/0031_regions.py b/netbox/dcim/migrations/0031_regions.py new file mode 100644 index 000000000..256bd2743 --- /dev/null +++ b/netbox/dcim/migrations/0031_regions.py @@ -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'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 622019997..dc0a676d6 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 0a891efea..2a013b48d 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -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 = """ """ +REGION_ACTIONS = """ +{% if perms.dcim.change_region %} + +{% endif %} +""" + RACKGROUP_ACTIONS = """ {% if perms.dcim.change_rackgroup %} @@ -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', + ) # diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index b7c417111..3c56ab109 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -17,6 +17,7 @@ class SiteTest(APITestCase): 'id', 'name', 'slug', + 'region', 'tenant', 'facility', 'asn', diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 1b337ad6e..7fde6e9b3 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -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\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'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 243d97e92..02d9acd10 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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') ) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6eef522ec..71e261dce 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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',) diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 4e63cf337..90bb3ad62 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -37,6 +37,11 @@
  • Import Sites
  • {% endif %}
  • +
  • Regions
  • + {% if perms.dcim.add_region %} +
  • Add a Region
  • + {% endif %} +
  • Tenants
  • {% if perms.tenancy.add_tenant %}
  • Add a Tenant
  • diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index ab54b45a5..f311ccb73 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -66,6 +66,10 @@ Tenant {% if circuit.tenant %} + {% if circuit.tenant.group %} + {{ circuit.tenant.group.name }} + + {% endif %} {{ circuit.tenant }} {% else %} None diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index ba0f8b5fe..948ccfb9a 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -27,6 +27,10 @@ Site + {% if termination.site.region %} + {{ termination.site.region }} + + {% endif %} {{ termination.site }} @@ -34,7 +38,8 @@ Termination {% if termination.interface %} - {{ termination.interface.device }} {{ termination.interface }} + {{ termination.interface.device }} + {{ termination.interface }} {% else %} Not defined {% endif %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index bcbaa1b0d..081397774 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -14,19 +14,13 @@ Device - - - - @@ -34,7 +28,11 @@ + + + + @@ -91,6 +95,10 @@
    Tenant - {% if device.tenant %} - {{ device.tenant }} - {% else %} - None - {% endif %} -
    Site + {% if device.site.region %} + {{ device.site.region }} + + {% endif %} {{ device.site }}
    Rack {% if device.rack %} - {{ device.rack.name }}{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %} + {% if device.rack.group %} + {{ device.rack.group.name }} + + {% endif %} + {{ device.rack.name }}{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %} {% else %} None {% endif %} @@ -57,6 +55,20 @@ {% endif %}
    Tenant + {% if device.tenant %} + {% if device.tenant.group %} + {{ device.tenant.group.name }} + + {% endif %} + {{ device.tenant }} + {% else %} + None + {% endif %} +
    Device Type diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 37cddf213..d6529c2a4 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -64,6 +64,10 @@
    Site + {% if rack.site.region %} + {{ rack.site.region }} + + {% endif %} {{ rack.site }}
    Tenant {% if rack.tenant %} + {% if rack.tenant.group %} + {{ rack.tenant.group.name }} + + {% endif %} {{ rack.tenant }} {% else %} None diff --git a/netbox/templates/dcim/region_list.html b/netbox/templates/dcim/region_list.html new file mode 100644 index 000000000..b54201a34 --- /dev/null +++ b/netbox/templates/dcim/region_list.html @@ -0,0 +1,21 @@ +{% extends '_base.html' %} +{% load helpers %} + +{% block title %}Regions{% endblock %} + +{% block content %} +
    + {% if perms.dcim.add_region %} + + + Add a region + + {% endif %} +
    +

    {{ block.title }}

    +
    +
    + {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %} +
    +
    +{% endblock %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 210ec0c82..890d34926 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -10,6 +10,9 @@
    @@ -55,10 +58,24 @@ Site + + + + +
    Region + {% if site.region %} + {{ site.region }} + {% else %} + None + {% endif %} +
    Tenant {% if site.tenant %} + {% if site.tenant.group %} + {{ site.tenant.group.name }} + + {% endif %} {{ site.tenant }} {% else %} None @@ -85,6 +102,13 @@ {% endif %}
    + +
    +
    + Contact Info +
    + + + + + + @@ -71,7 +76,7 @@
    Physical Address diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index d1f211adb..98f16ad25 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -7,6 +7,7 @@
    {% render_field form.name %} {% render_field form.slug %} + {% render_field form.region %} {% render_field form.tenant %} {% render_field form.facility %} {% render_field form.asn %} diff --git a/netbox/templates/dcim/site_import.html b/netbox/templates/dcim/site_import.html index 3018cc2f1..a7ac47ab5 100644 --- a/netbox/templates/dcim/site_import.html +++ b/netbox/templates/dcim/site_import.html @@ -38,6 +38,11 @@
    URL-friendly name ash4-south
    RegionName of region (optional)North America
    Tenant Name of tenant (optional)

    Example

    -
    ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com
    +
    ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com
    {% endblock %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 4ad5dba05..9187b81b4 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -30,8 +30,16 @@
    Tenant {% if prefix.tenant %} + {% if prefix.tenant.group %} + {{ prefix.tenant.group.name }} + + {% endif %} {{ prefix.tenant }} {% elif prefix.vrf.tenant %} + {% if prefix.vrf.tenant.group %} + {{ prefix.vrf.tenant.group.name }} + + {% endif %} {{ prefix.vrf.tenant }} {% else %} @@ -53,6 +61,10 @@ Site {% if prefix.site %} + {% if prefix.site.region %} + {{ prefix.site.region }} + + {% endif %} {{ prefix.site }} {% else %} None @@ -63,6 +75,10 @@ VLAN {% if prefix.vlan %} + {% if prefix.vlan.group %} + {{ prefix.vlan.group.name }} + + {% endif %} {{ prefix.vlan.display_name }} {% else %} None @@ -79,7 +95,7 @@ Role {% if prefix.role %} - {{ prefix.role }} + {{ prefix.role }} {% else %} None {% endif %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 1b6ddb5dd..6c1fb07d2 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -57,6 +57,10 @@ Site {% if vlan.site %} + {% if vlan.site.region %} + {{ vlan.site.region }} + + {% endif %} {{ vlan.site }} {% else %} None @@ -85,6 +89,10 @@ Tenant {% if vlan.tenant %} + {% if vlan.tenant.group %} + {{ vlan.tenant.group.name }} + + {% endif %} {{ vlan.tenant }} {% else %} None @@ -101,7 +109,7 @@ Role {% if vlan.role %} - {{ vlan.role }} + {{ vlan.role }} {% else %} None {% endif %}