mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merged develop
This commit is contained in:
@ -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 optionally be arranged by geographic region. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
|
@ -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,
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
|
from mptt.admin import MPTTModelAdmin
|
||||||
|
|
||||||
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(MPTTModelAdmin):
|
||||||
|
list_display = ['name', 'parent', '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']
|
||||||
|
@ -6,18 +6,46 @@ from dcim.models import (
|
|||||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
||||||
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
||||||
RACK_WIDTH_CHOICES, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||||
)
|
)
|
||||||
from extras.api.serializers import CustomFieldModelSerializer
|
from extras.api.serializers import CustomFieldModelSerializer
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer
|
from utilities.api import ChoiceFieldSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class NestedRegionSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class RegionSerializer(serializers.ModelSerializer):
|
||||||
|
parent = NestedRegionSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = ['id', 'url', 'name', 'slug', 'parent']
|
||||||
|
|
||||||
|
|
||||||
|
class WritableRegionSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = ['id', 'name', 'slug', 'parent']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteSerializer(CustomFieldModelSerializer):
|
class SiteSerializer(CustomFieldModelSerializer):
|
||||||
|
region = NestedRegionSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -10,6 +10,7 @@ from . import views
|
|||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
|
router.register(r'regions', views.RegionViewSet)
|
||||||
router.register(r'sites', views.SiteViewSet)
|
router.register(r'sites', views.SiteViewSet)
|
||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
|
@ -13,7 +13,7 @@ from dcim.models import (
|
|||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||||
RackRole, Site,
|
RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
from dcim import filters
|
||||||
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
||||||
@ -25,6 +25,16 @@ from .exceptions import MissingFilterException
|
|||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||||
|
queryset = Region.objects.all()
|
||||||
|
serializer_class = serializers.RegionSerializer
|
||||||
|
write_serializer_class = serializers.WritableRegionSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
@ -10,7 +10,7 @@ from .models import (
|
|||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackReservation, RackRole, Site, VIRTUAL_IFACE_TYPES,
|
RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +19,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(),
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -11,7 +13,7 @@ from tenancy.models import Tenant
|
|||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||||
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||||
SmallTextarea, SlugField,
|
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
@ -20,7 +22,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,18 +65,33 @@ 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 = ['parent', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteForm(BootstrapMixin, CustomFieldForm):
|
class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
region = TreeNodeChoiceField(queryset=Region.objects.all())
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
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 +106,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 +130,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 = TreeNodeChoiceField(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 = FilterTreeNodeMultipleChoiceField(
|
||||||
null_option=(0, 'None'))
|
queryset=Region.objects.annotate(filter_count=Count('sites')),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
38
netbox/dcim/migrations/0031_regions.py
Normal file
38
netbox/dcim/migrations/0031_regions.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-28 17:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
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)),
|
||||||
|
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('level', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -200,6 +202,29 @@ RPC_CLIENT_CHOICES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Region(MPTTModel):
|
||||||
|
"""
|
||||||
|
Sites can be grouped within geographic Regions.
|
||||||
|
"""
|
||||||
|
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
|
||||||
|
name = models.CharField(max_length=50, unique=True)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
|
||||||
|
class MPTTMeta:
|
||||||
|
order_insertion_by = ['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 +243,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)
|
||||||
@ -244,6 +270,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return csv_format([
|
return csv_format([
|
||||||
self.name,
|
self.name,
|
||||||
self.slug,
|
self.slug,
|
||||||
|
self.region.name if self.region else None,
|
||||||
self.tenant.name if self.tenant else None,
|
self.tenant.name if self.tenant else None,
|
||||||
self.facility,
|
self.facility,
|
||||||
self.asn,
|
self.asn,
|
||||||
@ -1248,8 +1275,8 @@ class Interface(models.Model):
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
# A LAG interface cannot have a parent LAG
|
# A virtual interface cannot have a parent LAG
|
||||||
if self.form_factor == IFACE_FF_LAG and self.lag is not None:
|
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||||
})
|
})
|
||||||
|
@ -6,10 +6,28 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REGION_LINK = """
|
||||||
|
{% if record.get_children %}
|
||||||
|
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i></a>
|
||||||
|
{% else %}
|
||||||
|
<span style="padding-left: {{ record.get_ancestors|length }}9px">
|
||||||
|
{% endif %}
|
||||||
|
{{ record.name }}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
|
||||||
|
SITE_REGION_LINK = """
|
||||||
|
{% if record.region %}
|
||||||
|
<a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
COLOR_LABEL = """
|
COLOR_LABEL = """
|
||||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||||
"""
|
"""
|
||||||
@ -20,6 +38,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 +100,27 @@ UTILIZATION_GRAPH = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
# name = tables.LinkColumn(verbose_name='Name')
|
||||||
|
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
|
||||||
|
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 +129,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.TemplateColumn(template_code=SITE_REGION_LINK, 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 +140,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',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -17,6 +17,7 @@ class SiteTest(APITestCase):
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'slug',
|
'slug',
|
||||||
|
'region',
|
||||||
'tenant',
|
'tenant',
|
||||||
'facility',
|
'facility',
|
||||||
'asn',
|
'asn',
|
||||||
|
@ -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'),
|
||||||
|
@ -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')
|
||||||
)
|
)
|
||||||
|
@ -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',)
|
||||||
|
@ -104,6 +104,7 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.humanize',
|
'django.contrib.humanize',
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
|
'mptt',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_swagger',
|
'rest_framework_swagger',
|
||||||
'circuits',
|
'circuits',
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
@ -393,7 +405,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces or device.device_type.is_network_device %}
|
{% if interfaces or device.device_type.is_network_device %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||||
|
@ -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>
|
||||||
|
21
netbox/templates/dcim/region_list.html
Normal file
21
netbox/templates/dcim/region_list.html
Normal 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 %}
|
@ -9,7 +9,12 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<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>
|
{% if site.region %}
|
||||||
|
{% for region in site.region.get_ancestors %}
|
||||||
|
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<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 +60,28 @@
|
|||||||
<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 %}
|
||||||
|
{% for region in site.region.get_ancestors %}
|
||||||
|
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endfor %}
|
||||||
|
<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 +108,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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -2,6 +2,8 @@ import csv
|
|||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from mptt.forms import TreeNodeMultipleChoiceField
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
@ -365,7 +367,7 @@ class SlugField(forms.SlugField):
|
|||||||
self.widget.attrs['slug-source'] = slug_source
|
self.widget.attrs['slug-source'] = slug_source
|
||||||
|
|
||||||
|
|
||||||
class FilterChoiceField(forms.ModelMultipleChoiceField):
|
class FilterChoiceFieldMixin(object):
|
||||||
iterator = forms.models.ModelChoiceIterator
|
iterator = forms.models.ModelChoiceIterator
|
||||||
|
|
||||||
def __init__(self, null_option=None, *args, **kwargs):
|
def __init__(self, null_option=None, *args, **kwargs):
|
||||||
@ -374,12 +376,13 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
|
|||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
if 'widget' not in kwargs:
|
if 'widget' not in kwargs:
|
||||||
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
|
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
|
||||||
super(FilterChoiceField, self).__init__(*args, **kwargs)
|
super(FilterChoiceFieldMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj):
|
||||||
|
label = super(FilterChoiceFieldMixin, self).label_from_instance(obj)
|
||||||
if hasattr(obj, 'filter_count'):
|
if hasattr(obj, 'filter_count'):
|
||||||
return u'{} ({})'.format(obj, obj.filter_count)
|
return u'{} ({})'.format(label, obj.filter_count)
|
||||||
return force_text(obj)
|
return label
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
if hasattr(self, '_choices'):
|
if hasattr(self, '_choices'):
|
||||||
@ -391,6 +394,14 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
|
|||||||
choices = property(_get_choices, forms.ChoiceField._set_choices)
|
choices = property(_get_choices, forms.ChoiceField._set_choices)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultipleChoiceField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LaxURLField(forms.URLField):
|
class LaxURLField(forms.URLField):
|
||||||
"""
|
"""
|
||||||
Custom URLField which allows any valid URL scheme
|
Custom URLField which allows any valid URL scheme
|
||||||
|
@ -3,6 +3,7 @@ cryptography>=1.4
|
|||||||
Django>=1.10
|
Django>=1.10
|
||||||
django-debug-toolbar>=1.6
|
django-debug-toolbar>=1.6
|
||||||
django-filter==0.15.3
|
django-filter==0.15.3
|
||||||
|
django-mptt==0.8.7
|
||||||
django-rest-swagger==0.3.10
|
django-rest-swagger==0.3.10
|
||||||
django-tables2>=1.2.5
|
django-tables2>=1.2.5
|
||||||
djangorestframework>=3.5.0
|
djangorestframework>=3.5.0
|
||||||
|
Reference in New Issue
Block a user