mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 11:37:21 -06:00
Closes #49: Introduction of circuit terminations
This commit is contained in:
parent
298ac1ba7a
commit
bf817eb69e
@ -21,11 +21,9 @@ class CircuitTypeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Circuit)
|
@admin.register(Circuit)
|
||||||
class CircuitAdmin(admin.ModelAdmin):
|
class CircuitAdmin(admin.ModelAdmin):
|
||||||
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
|
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate_human']
|
||||||
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
|
|
||||||
list_filter = ['provider', 'type', 'tenant']
|
list_filter = ['provider', 'type', 'tenant']
|
||||||
exclude = ['interface']
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(CircuitAdmin, self).get_queryset(request)
|
qs = super(CircuitAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('provider', 'type', 'tenant', 'site')
|
return qs.select_related('provider', 'type', 'tenant')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from circuits.models import Provider, CircuitType, Circuit
|
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||||
from extras.api.serializers import CustomFieldSerializer
|
from extras.api.serializers import CustomFieldSerializer
|
||||||
from tenancy.api.serializers import TenantNestedSerializer
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
@ -45,17 +45,24 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||||
provider = ProviderNestedSerializer()
|
|
||||||
type = CircuitTypeNestedSerializer()
|
|
||||||
tenant = TenantNestedSerializer()
|
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
interface = InterfaceNestedSerializer()
|
interface = InterfaceNestedSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CircuitTermination
|
||||||
|
fields = ['id', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info']
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||||
|
provider = ProviderNestedSerializer()
|
||||||
|
type = CircuitTypeNestedSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
|
terminations = CircuitTerminationSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
|
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'comments', 'terminations', 'custom_fields']
|
||||||
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitNestedSerializer(CircuitSerializer):
|
class CircuitNestedSerializer(CircuitSerializer):
|
||||||
|
@ -43,7 +43,7 @@ class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List circuits (filterable)
|
List circuits (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
|
||||||
.prefetch_related('custom_field_values__field')
|
.prefetch_related('custom_field_values__field')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
filter_class = CircuitFilter
|
filter_class = CircuitFilter
|
||||||
@ -53,6 +53,6 @@ class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single circuit
|
Retrieve a single circuit
|
||||||
"""
|
"""
|
||||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
|
||||||
.prefetch_related('custom_field_values__field')
|
.prefetch_related('custom_field_values__field')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
|
@ -16,12 +16,12 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='circuits__site',
|
name='circuits__terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site',
|
label='Site',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='circuits__site',
|
name='circuits__terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -78,12 +78,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -91,12 +91,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'install_date']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(cid__icontains=value) |
|
Q(cid__icontains=value) |
|
||||||
Q(xconnect_id__icontains=value) |
|
Q(xconnect_id__icontains=value) |
|
||||||
Q(pp_info__icontains=value) |
|
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from utilities.forms import (
|
|||||||
SlugField,
|
SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -82,6 +82,64 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Circuit
|
||||||
|
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'comments']
|
||||||
|
help_texts = {
|
||||||
|
'cid': "Unique circuit ID",
|
||||||
|
'install_date': "Format: YYYY-MM-DD",
|
||||||
|
'commit_rate': "Commited rate",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitFromCSVForm(forms.ModelForm):
|
||||||
|
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Provider not found.'})
|
||||||
|
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Circuit
|
||||||
|
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate']
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||||
|
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||||
|
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||||
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
|
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||||
|
comments = CommentField(widget=SmallTextarea)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['tenant', 'commit_rate', 'comments']
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
model = Circuit
|
||||||
|
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||||
|
to_field_name='slug')
|
||||||
|
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||||
|
to_field_name='slug')
|
||||||
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||||
|
null_option=(0, 'None'))
|
||||||
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||||
|
to_field_name='slug')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Circuit terminations
|
||||||
|
#
|
||||||
|
|
||||||
|
class CircuitTerminationForm(forms.ModelForm, BootstrapMixin):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
||||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
@ -95,28 +153,25 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||||
disabled_indicator='is_connected'))
|
disabled_indicator='is_connected'))
|
||||||
comments = CommentField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = CircuitTermination
|
||||||
fields = [
|
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
|
||||||
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
'xconnect_id', 'pp_info']
|
||||||
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
|
||||||
]
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
|
||||||
'install_date': "Format: YYYY-MM-DD",
|
|
||||||
'port_speed': "Physical circuit speed",
|
'port_speed': "Physical circuit speed",
|
||||||
'commit_rate': "Commited rate",
|
|
||||||
'xconnect_id': "ID of the local cross-connect",
|
'xconnect_id': "ID of the local cross-connect",
|
||||||
'pp_info': "Patch panel ID and port number(s)"
|
'pp_info': "Patch panel ID and port number(s)"
|
||||||
}
|
}
|
||||||
|
widgets = {
|
||||||
|
'term_side': forms.HiddenInput(),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super(CircuitForm, self).__init__(*args, **kwargs)
|
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# If this circuit has been assigned to an interface, initialize rack and device
|
# If an interface has been assigned, initialize rack and device
|
||||||
if self.instance.interface:
|
if self.instance.interface:
|
||||||
self.initial['rack'] = self.instance.interface.device.rack
|
self.initial['rack'] = self.instance.interface.device.rack
|
||||||
self.initial['device'] = self.instance.interface.device
|
self.initial['device'] = self.instance.interface.device
|
||||||
@ -140,11 +195,13 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
# Limit interface choices
|
# Limit interface choices
|
||||||
if self.is_bound and self.data.get('device'):
|
if self.is_bound and self.data.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
interfaces = Interface.objects.filter(device=self.data['device'])\
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||||
|
'connected_as_b')
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||||
elif self.initial.get('device'):
|
elif self.initial.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||||
|
'connected_as_b')
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||||
else:
|
else:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
@ -154,47 +211,3 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||||
}) for iface in interfaces
|
}) for iface in interfaces
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitFromCSVForm(forms.ModelForm):
|
|
||||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
|
||||||
error_messages={'invalid_choice': 'Provider not found.'})
|
|
||||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
|
||||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
|
||||||
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
|
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Circuit
|
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
|
|
||||||
'commit_rate', 'xconnect_id', 'pp_info']
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
|
||||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
|
||||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
|
||||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
|
||||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
|
||||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
|
||||||
comments = CommentField(widget=SmallTextarea)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|
||||||
model = Circuit
|
|
||||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
|
||||||
to_field_name='slug')
|
|
||||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
|
||||||
to_field_name='slug')
|
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
|
||||||
null_option=(0, 'None'))
|
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuits')), to_field_name='slug')
|
|
||||||
|
99
netbox/circuits/migrations/0006_terminations.py
Normal file
99
netbox/circuits/migrations/0006_terminations.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10 on 2016-12-13 16:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def circuits_to_terms(apps, schema_editor):
|
||||||
|
Circuit = apps.get_model('circuits', 'Circuit')
|
||||||
|
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||||
|
for c in Circuit.objects.all():
|
||||||
|
CircuitTermination(
|
||||||
|
circuit=c,
|
||||||
|
term_side=b'A',
|
||||||
|
site=c.site,
|
||||||
|
interface=c.interface,
|
||||||
|
port_speed=c.port_speed,
|
||||||
|
upstream_speed=c.upstream_speed,
|
||||||
|
xconnect_id=c.xconnect_id,
|
||||||
|
pp_info=c.pp_info,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
|
||||||
|
def terms_to_circuits(apps, schema_editor):
|
||||||
|
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||||
|
for ct in CircuitTermination.objects.filter(term_side='A'):
|
||||||
|
c = ct.circuit
|
||||||
|
c.site = ct.site
|
||||||
|
c.interface = ct.interface
|
||||||
|
c.port_speed = ct.port_speed
|
||||||
|
c.upstream_speed = ct.upstream_speed
|
||||||
|
c.xconnect_id = ct.xconnect_id
|
||||||
|
c.pp_info = ct.pp_info
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0022_color_names_to_rgb'),
|
||||||
|
('circuits', '0005_circuit_add_upstream_speed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CircuitTermination',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1,
|
||||||
|
verbose_name='Termination')),
|
||||||
|
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
|
||||||
|
('upstream_speed',
|
||||||
|
models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed',
|
||||||
|
null=True, verbose_name=b'Upstream speed (Kbps)')),
|
||||||
|
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
|
||||||
|
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
|
||||||
|
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations',
|
||||||
|
to='circuits.Circuit')),
|
||||||
|
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='circuit_termination', to='dcim.Interface')),
|
||||||
|
('site',
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations',
|
||||||
|
to='dcim.Site')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['circuit', 'term_side'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='circuittermination',
|
||||||
|
unique_together=set([('circuit', 'term_side')]),
|
||||||
|
),
|
||||||
|
migrations.RunPython(circuits_to_terms, terms_to_circuits),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='interface',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='port_speed',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='pp_info',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='site',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='upstream_speed',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='xconnect_id',
|
||||||
|
),
|
||||||
|
]
|
@ -3,12 +3,35 @@ from django.core.urlresolvers import reverse
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from dcim.models import Site, Interface
|
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomFieldValue
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
|
|
||||||
|
TERM_SIDE_A = 'A'
|
||||||
|
TERM_SIDE_Z = 'Z'
|
||||||
|
TERM_SIDE_CHOICES = (
|
||||||
|
(TERM_SIDE_A, 'A'),
|
||||||
|
(TERM_SIDE_Z, 'Z'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def humanize_speed(speed):
|
||||||
|
"""
|
||||||
|
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
||||||
|
"""
|
||||||
|
if speed >= 1000000000 and speed % 1000000000 == 0:
|
||||||
|
return '{} Tbps'.format(speed / 1000000000)
|
||||||
|
elif speed >= 1000000 and speed % 1000000 == 0:
|
||||||
|
return '{} Gbps'.format(speed / 1000000)
|
||||||
|
elif speed >= 1000 and speed % 1000 == 0:
|
||||||
|
return '{} Mbps'.format(speed / 1000)
|
||||||
|
elif speed >= 1000:
|
||||||
|
return '{} Mbps'.format(float(speed) / 1000)
|
||||||
|
else:
|
||||||
|
return '{} Kbps'.format(speed)
|
||||||
|
|
||||||
|
|
||||||
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
@ -71,15 +94,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
||||||
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
|
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
|
||||||
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
|
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
|
|
||||||
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
|
|
||||||
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
||||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
|
||||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
|
||||||
help_text='Upstream speed, if different from port speed')
|
|
||||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
||||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
|
||||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||||
|
|
||||||
@ -99,42 +115,61 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.provider.name,
|
self.provider.name,
|
||||||
self.type.name,
|
self.type.name,
|
||||||
self.tenant.name if self.tenant else '',
|
self.tenant.name if self.tenant else '',
|
||||||
self.site.name,
|
|
||||||
self.install_date.isoformat() if self.install_date else '',
|
self.install_date.isoformat() if self.install_date else '',
|
||||||
str(self.port_speed),
|
|
||||||
str(self.upstream_speed),
|
|
||||||
str(self.commit_rate) if self.commit_rate else '',
|
str(self.commit_rate) if self.commit_rate else '',
|
||||||
self.xconnect_id,
|
|
||||||
self.pp_info,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def _humanize_speed(self, speed):
|
def _get_termination(self, side):
|
||||||
"""
|
for ct in self.terminations.all():
|
||||||
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
if ct.term_side == side:
|
||||||
"""
|
return ct
|
||||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
return None
|
||||||
return '{} Tbps'.format(speed / 1000000000)
|
|
||||||
elif speed >= 1000000 and speed % 1000000 == 0:
|
@property
|
||||||
return '{} Gbps'.format(speed / 1000000)
|
def termination_a(self):
|
||||||
elif speed >= 1000 and speed % 1000 == 0:
|
return self._get_termination('A')
|
||||||
return '{} Mbps'.format(speed / 1000)
|
|
||||||
elif speed >= 1000:
|
@property
|
||||||
return '{} Mbps'.format(float(speed) / 1000)
|
def termination_z(self):
|
||||||
else:
|
return self._get_termination('Z')
|
||||||
return '{} Kbps'.format(speed)
|
|
||||||
|
def commit_rate_human(self):
|
||||||
|
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
|
||||||
|
commit_rate_human.admin_order_field = 'commit_rate'
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTermination(models.Model):
|
||||||
|
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
||||||
|
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
||||||
|
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
|
||||||
|
interface = models.OneToOneField('dcim.Interface', related_name='circuit_termination', blank=True, null=True)
|
||||||
|
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
||||||
|
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||||
|
help_text='Upstream speed, if different from port speed')
|
||||||
|
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
||||||
|
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['circuit', 'term_side']
|
||||||
|
unique_together = ['circuit', 'term_side']
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||||
|
|
||||||
|
def get_parent_url(self):
|
||||||
|
return self.circuit.get_absolute_url()
|
||||||
|
|
||||||
|
def get_peer_termination(self):
|
||||||
|
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||||
|
try:
|
||||||
|
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
|
||||||
|
except CircuitTermination.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
def port_speed_human(self):
|
def port_speed_human(self):
|
||||||
return self._humanize_speed(self.port_speed)
|
return humanize_speed(self.port_speed)
|
||||||
port_speed_human.admin_order_field = 'port_speed'
|
port_speed_human.admin_order_field = 'port_speed'
|
||||||
|
|
||||||
def upstream_speed_human(self):
|
def upstream_speed_human(self):
|
||||||
if not self.upstream_speed:
|
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
|
||||||
return ''
|
|
||||||
return self._humanize_speed(self.upstream_speed)
|
|
||||||
upstream_speed_human.admin_order_field = 'upstream_speed'
|
upstream_speed_human.admin_order_field = 'upstream_speed'
|
||||||
|
|
||||||
def commit_rate_human(self):
|
|
||||||
if not self.commit_rate:
|
|
||||||
return ''
|
|
||||||
return self._humanize_speed(self.commit_rate)
|
|
||||||
commit_rate_human.admin_order_field = 'commit_rate'
|
|
||||||
|
@ -56,12 +56,13 @@ class CircuitTable(BaseTable):
|
|||||||
type = tables.Column(verbose_name='Type')
|
type = tables.Column(verbose_name='Type')
|
||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||||
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
args=[Accessor('termination_a.site.slug')])
|
||||||
verbose_name='Port Speed')
|
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||||
|
args=[Accessor('termination_z.site.slug')])
|
||||||
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
||||||
verbose_name='Commit Rate')
|
verbose_name='Commit Rate')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate')
|
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'commit_rate')
|
||||||
|
@ -30,5 +30,11 @@ urlpatterns = [
|
|||||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
||||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||||
|
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||||
|
|
||||||
|
# Circuit terminations
|
||||||
|
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||||
|
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||||
|
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -27,7 +31,7 @@ class ProviderListView(ObjectListView):
|
|||||||
def provider(request, slug):
|
def provider(request, slug):
|
||||||
|
|
||||||
provider = get_object_or_404(Provider, slug=slug)
|
provider = get_object_or_404(Provider, slug=slug)
|
||||||
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
circuits = Circuit.objects.filter(provider=provider)
|
||||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||||
|
|
||||||
return render(request, 'circuits/provider.html', {
|
return render(request, 'circuits/provider.html', {
|
||||||
@ -103,7 +107,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitListView(ObjectListView):
|
class CircuitListView(ObjectListView):
|
||||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
|
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
@ -114,9 +118,13 @@ 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, 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()
|
||||||
|
|
||||||
return render(request, 'circuits/circuit.html', {
|
return render(request, 'circuits/circuit.html', {
|
||||||
'circuit': circuit,
|
'circuit': circuit,
|
||||||
|
'termination_a': termination_a,
|
||||||
|
'termination_z': termination_z,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -124,7 +132,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'circuits.change_circuit'
|
permission_required = 'circuits.change_circuit'
|
||||||
model = Circuit
|
model = Circuit
|
||||||
form_class = forms.CircuitForm
|
form_class = forms.CircuitForm
|
||||||
fields_initial = ['site']
|
fields_initial = ['provider']
|
||||||
template_name = 'circuits/circuit_edit.html'
|
template_name = 'circuits/circuit_edit.html'
|
||||||
obj_list_url = 'circuits:circuit_list'
|
obj_list_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
@ -155,3 +163,71 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
default_redirect_url = 'circuits:circuit_list'
|
default_redirect_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required('circuits.change_circuittermination')
|
||||||
|
def circuit_terminations_swap(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()
|
||||||
|
if not termination_a and not termination_z:
|
||||||
|
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
|
||||||
|
return redirect('circuits:circuit', pk=circuit.pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ConfirmationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
if termination_a and termination_z:
|
||||||
|
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
|
||||||
|
with transaction.atomic():
|
||||||
|
termination_a.term_side = '_'
|
||||||
|
termination_a.save()
|
||||||
|
termination_z.term_side = 'A'
|
||||||
|
termination_z.save()
|
||||||
|
termination_a.term_side = 'Z'
|
||||||
|
termination_a.save()
|
||||||
|
elif termination_a:
|
||||||
|
termination_a.term_side = 'Z'
|
||||||
|
termination_a.save()
|
||||||
|
else:
|
||||||
|
termination_z.term_side = 'A'
|
||||||
|
termination_z.save()
|
||||||
|
messages.success(request, "Swapped terminations for circuit {}.".format(circuit))
|
||||||
|
return redirect('circuits:circuit', pk=circuit.pk)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = ConfirmationForm()
|
||||||
|
|
||||||
|
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||||
|
'circuit': circuit,
|
||||||
|
'termination_a': termination_a,
|
||||||
|
'termination_z': termination_z,
|
||||||
|
'form': form,
|
||||||
|
'panel_class': 'default',
|
||||||
|
'button_class': 'primary',
|
||||||
|
'cancel_url': circuit.get_absolute_url(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Circuit terminations
|
||||||
|
#
|
||||||
|
|
||||||
|
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'circuits.change_circuittermination'
|
||||||
|
model = CircuitTermination
|
||||||
|
form_class = forms.CircuitTerminationForm
|
||||||
|
fields_initial = ['term_side']
|
||||||
|
template_name = 'circuits/circuittermination_edit.html'
|
||||||
|
|
||||||
|
def alter_obj(self, obj, args, kwargs):
|
||||||
|
if 'circuit' in kwargs:
|
||||||
|
circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
||||||
|
obj.circuit = circuit
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'circuits.delete_circuittermination'
|
||||||
|
model = CircuitTermination
|
||||||
|
@ -484,7 +484,7 @@ class RelatedConnectionsView(APIView):
|
|||||||
|
|
||||||
# Interface connections
|
# Interface connections
|
||||||
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
||||||
'circuit')
|
'circuit_termination')
|
||||||
for iface in interfaces:
|
for iface in interfaces:
|
||||||
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||||
del(data['device'])
|
del(data['device'])
|
||||||
|
@ -9,6 +9,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
|
|
||||||
|
from circuits.models import Circuit
|
||||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -285,7 +286,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def count_circuits(self):
|
def count_circuits(self):
|
||||||
return self.circuits.count()
|
return Circuit.objects.filter(terminations__site=self).count()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1136,7 +1137,7 @@ class Interface(models.Model):
|
|||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
try:
|
try:
|
||||||
return bool(self.circuit)
|
return bool(self.circuit_termination)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
return bool(self.connection)
|
return bool(self.connection)
|
||||||
|
@ -78,7 +78,7 @@ def site(request, slug):
|
|||||||
'device_count': Device.objects.filter(rack__site=site).count(),
|
'device_count': Device.objects.filter(rack__site=site).count(),
|
||||||
'prefix_count': Prefix.objects.filter(site=site).count(),
|
'prefix_count': Prefix.objects.filter(site=site).count(),
|
||||||
'vlan_count': VLAN.objects.filter(site=site).count(),
|
'vlan_count': VLAN.objects.filter(site=site).count(),
|
||||||
'circuit_count': Circuit.objects.filter(site=site).count(),
|
'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
|
||||||
}
|
}
|
||||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||||
topology_maps = TopologyMap.objects.filter(site=site)
|
topology_maps = TopologyMap.objects.filter(site=site)
|
||||||
@ -561,9 +561,9 @@ def device(request, pk):
|
|||||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination__circuit')
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination__circuit')
|
||||||
device_bays = natsorted(
|
device_bays = natsorted(
|
||||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||||
key=attrgetter('name')
|
key=attrgetter('name')
|
||||||
|
@ -82,17 +82,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>Speed</td>
|
|
||||||
<td>
|
|
||||||
{% if circuit.upstream_speed %}
|
|
||||||
<i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }}
|
|
||||||
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
|
|
||||||
{% else %}
|
|
||||||
{{ circuit.port_speed_human }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Commit Rate</td>
|
<td>Commit Rate</td>
|
||||||
<td>
|
<td>
|
||||||
@ -108,66 +97,6 @@
|
|||||||
{% with circuit.get_custom_fields as custom_fields %}
|
{% with circuit.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Termination</strong>
|
|
||||||
</div>
|
|
||||||
<table class="table table-hover panel-body">
|
|
||||||
<tr>
|
|
||||||
<td>Site</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Termination</td>
|
|
||||||
<td>
|
|
||||||
{% if circuit.interface %}
|
|
||||||
<span><a href="{% url 'dcim:device' pk=circuit.interface.device.pk %}">{{ circuit.interface.device }}</a> {{ circuit.interface }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Not defined</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>IP Addressing</td>
|
|
||||||
<td>
|
|
||||||
{% if circuit.interface %}
|
|
||||||
{% for ip in circuit.interface.ip_addresses.all %}
|
|
||||||
{% if not forloop.first %}<br />{% endif %}
|
|
||||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
|
||||||
{% empty %}
|
|
||||||
<span class="text-muted">None</span>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">N/A</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Cross-Connect</td>
|
|
||||||
<td>
|
|
||||||
{% if circuit.xconnect_id %}
|
|
||||||
{{ circuit.xconnect_id }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">N/A</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Patch Panel/Port</td>
|
|
||||||
<td>
|
|
||||||
{% if circuit.pp_info %}
|
|
||||||
{{ circuit.pp_info }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">N/A</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
@ -180,6 +109,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %}
|
||||||
|
{% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% extends 'utilities/obj_edit.html' %}
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
{% load static from staticfiles %}
|
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
@ -11,15 +10,6 @@
|
|||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
{% render_field form.install_date %}
|
{% render_field form.install_date %}
|
||||||
{% render_field form.xconnect_id %}
|
|
||||||
{% render_field form.pp_info %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading"><strong>Bandwidth</strong></div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% render_field form.port_speed %}
|
|
||||||
{% render_field form.upstream_speed %}
|
|
||||||
{% render_field form.commit_rate %}
|
{% render_field form.commit_rate %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -31,26 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading"><strong>Termination</strong></div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% render_field form.site %}
|
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
|
||||||
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
|
||||||
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
|
||||||
</ul>
|
|
||||||
<div class="tab-content">
|
|
||||||
<div class="tab-pane active" id="select">
|
|
||||||
{% render_field form.rack %}
|
|
||||||
{% render_field form.device %}
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="search">
|
|
||||||
{% render_field form.livesearch %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.interface %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Comments</strong></div>
|
<div class="panel-heading"><strong>Comments</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@ -58,7 +28,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script src="{% static 'js/livesearch.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
25
netbox/templates/circuits/circuit_terminations_swap.html
Normal file
25
netbox/templates/circuits/circuit_terminations_swap.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'utilities/confirmation_form.html' %}
|
||||||
|
|
||||||
|
{% block title %}Swap Circuit Terminations{% endblock %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
<p>Swap these terminations for circuit {{ circuit }}?</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>A side:</strong>
|
||||||
|
{% if termination_a %}
|
||||||
|
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Z side:</strong>
|
||||||
|
{% if termination_z %}
|
||||||
|
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
94
netbox/templates/circuits/circuittermination_edit.html
Normal file
94
netbox/templates/circuits/circuittermination_edit.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="post" class="form form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3>
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Location</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Provider</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ obj.circuit.provider }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Circuit</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ obj.circuit.cid }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Termination</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ form.term_side.value }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% render_field form.site %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||||
|
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active" id="select">
|
||||||
|
{% render_field form.rack %}
|
||||||
|
{% render_field form.device %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="search">
|
||||||
|
{% render_field form.livesearch %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% render_field form.interface %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Termination Details</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.port_speed %}
|
||||||
|
{% render_field form.upstream_speed %}
|
||||||
|
{% render_field form.xconnect_id %}
|
||||||
|
{% render_field form.pp_info %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3 text-right">
|
||||||
|
{% if obj.pk %}
|
||||||
|
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'js/livesearch.js' %}"></script>
|
||||||
|
{% endblock %}
|
95
netbox/templates/circuits/inc/circuit_termination.html
Normal file
95
netbox/templates/circuits/inc/circuit_termination.html
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="pull-right">
|
||||||
|
{% if not termination and perms.circuits.add_circuittermination %}
|
||||||
|
<a href="{% url 'circuits:circuittermination_add' circuit=circuit.pk %}?term_side={{ side }}" class="btn btn-xs btn-success">
|
||||||
|
<span class="fa fa-plus" aria-hidden="true"></span> Add
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if termination and perms.circuits.change_circuittermination %}
|
||||||
|
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-xs btn-warning">
|
||||||
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'circuits:circuit_terminations_swap' pk=circuit.pk %}" class="btn btn-xs btn-primary">
|
||||||
|
<span class="fa fa-refresh" aria-hidden="true"></span> Swap
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if termination and perms.circuits.delete_circuittermination %}
|
||||||
|
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}" class="btn btn-xs btn-danger">
|
||||||
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<strong>Termination - {{ side }} Side</strong>
|
||||||
|
</div>
|
||||||
|
{% if termination %}
|
||||||
|
<table class="table table-hover panel-body">
|
||||||
|
<tr>
|
||||||
|
<td>Site</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not defined</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Speed</td>
|
||||||
|
<td>
|
||||||
|
{% if termination.upstream_speed %}
|
||||||
|
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed_human }}
|
||||||
|
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed_human }}
|
||||||
|
{% else %}
|
||||||
|
{{ termination.port_speed_human }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IP Addressing</td>
|
||||||
|
<td>
|
||||||
|
{% if termination.interface %}
|
||||||
|
{% for ip in termination.interface.ip_addresses.all %}
|
||||||
|
{% if not forloop.first %}<br />{% endif %}
|
||||||
|
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cross-Connect</td>
|
||||||
|
<td>
|
||||||
|
{% if termination.xconnect_id %}
|
||||||
|
{{ termination.xconnect_id }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Patch Panel/Port</td>
|
||||||
|
<td>
|
||||||
|
{% if termination.pp_info %}
|
||||||
|
{{ termination.pp_info }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="panel-body">
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -134,14 +134,8 @@
|
|||||||
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:site' slug=c.site.slug %}">{{ c.site }}</a>
|
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
{% if c.interface %}
|
|
||||||
<a href="{% url 'dcim:device' pk=c.interface.device.pk %}">{{ c.interface.device }}</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ c.port_speed_human }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -149,6 +143,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.circuits.add_circuit %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add circuit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.platform %}
|
{% render_field form.platform %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% if obj %}
|
{% if obj.pk %}
|
||||||
{% render_field form.primary_ip4 %}
|
{% render_field form.primary_ip4 %}
|
||||||
{% render_field form.primary_ip6 %}
|
{% render_field form.primary_ip6 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<small>{{ iface.mac_address|default:'' }}</small>
|
<small>{{ iface.mac_address|default:'' }}</small>
|
||||||
</td>
|
</td>
|
||||||
{% if not iface.is_physical %}
|
{% if not iface.is_physical %}
|
||||||
<td colspan="2">Virtual</td>
|
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||||
{% elif iface.connection %}
|
{% elif iface.connection %}
|
||||||
{% with iface.get_connected_interface as connected_iface %}
|
{% with iface.get_connected_interface as connected_iface %}
|
||||||
<td>
|
<td>
|
||||||
@ -24,10 +24,16 @@
|
|||||||
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif iface.circuit %}
|
{% elif iface.circuit_termination %}
|
||||||
<td colspan="2">
|
{% with iface.circuit_termination.get_peer_termination as peer_termination %}
|
||||||
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a>
|
<td colspan="2">
|
||||||
</td>
|
<i class="fa fa-fw fa-globe"></i>
|
||||||
|
{% if peer_termination %}
|
||||||
|
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a> via
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
|
||||||
|
</td>
|
||||||
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
@ -35,7 +41,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if show_graphs %}
|
{% if show_graphs %}
|
||||||
{% if iface.circuit or iface.connection %}
|
{% if iface.circuit_termination or iface.connection %}
|
||||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
|
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
|
||||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -56,8 +62,11 @@
|
|||||||
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
|
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
|
||||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% elif iface.circuit and perms.circuits.change_circuit %}
|
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
|
||||||
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
|
<button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected">
|
||||||
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination">
|
||||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -71,7 +80,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
{% if iface.connection or iface.circuit %}
|
{% if iface.connection or iface.circuit_termination %}
|
||||||
<button class="btn btn-danger btn-xs" disabled="disabled">
|
<button class="btn btn-danger btn-xs" disabled="disabled">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% render_field form.vrf %}
|
{% render_field form.vrf %}
|
||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% if obj %}
|
{% if obj.pk %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">Device</label>
|
<label class="col-md-3 control-label">Device</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<form action="." method="post" class="form">
|
<form action="." method="post" class="form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-{{ panel_class|default:"danger" }}">
|
||||||
<div class="panel-heading">{% block title %}{% endblock %}</div>
|
<div class="panel-heading">{% block title %}{% endblock %}</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<button type="submit" name="_confirm" class="btn btn-danger">Confirm</button>
|
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,15 +2,18 @@
|
|||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if obj %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
|
{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<h3>{% if obj %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}</h3>
|
<h3>{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}</h3>
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
@ -31,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3 text-right">
|
<div class="col-md-6 col-md-offset-3 text-right">
|
||||||
{% if obj %}
|
{% if obj.pk %}
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
@ -123,28 +123,32 @@ class ObjectEditView(View):
|
|||||||
use_obj_view = True
|
use_obj_view = True
|
||||||
|
|
||||||
def get_object(self, kwargs):
|
def get_object(self, kwargs):
|
||||||
# Look up object by slug if one has been provided. Otherwise, use PK.
|
# Look up object by slug or PK. Return None if neither was provided.
|
||||||
if 'slug' in kwargs:
|
if 'slug' in kwargs:
|
||||||
return get_object_or_404(self.model, slug=kwargs['slug'])
|
return get_object_or_404(self.model, slug=kwargs['slug'])
|
||||||
else:
|
elif 'pk' in kwargs:
|
||||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||||
|
return self.model()
|
||||||
|
|
||||||
|
def alter_obj(self, obj, args, kwargs):
|
||||||
|
# Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
|
||||||
|
# given some parameter from the request URI.
|
||||||
|
return obj
|
||||||
|
|
||||||
def get_redirect_url(self, obj):
|
def get_redirect_url(self, obj):
|
||||||
if obj and self.use_obj_view:
|
# Determine where to redirect the user after updating an object (or aborting an update).
|
||||||
if hasattr(obj, 'get_absolute_url'):
|
if obj.pk and self.use_obj_view and hasattr(obj, 'get_absolute_url'):
|
||||||
return obj.get_absolute_url()
|
return obj.get_absolute_url()
|
||||||
if hasattr(obj, 'get_parent_url'):
|
if obj and self.use_obj_view and hasattr(obj, 'get_parent_url'):
|
||||||
return obj.get_parent_url()
|
return obj.get_parent_url()
|
||||||
return reverse(self.obj_list_url)
|
return reverse(self.obj_list_url)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
if kwargs:
|
obj = self.get_object(kwargs)
|
||||||
obj = self.get_object(kwargs)
|
obj = self.alter_obj(obj, args, kwargs)
|
||||||
form = self.form_class(instance=obj)
|
initial_data = {k: request.GET[k] for k in self.fields_initial if k in request.GET}
|
||||||
else:
|
form = self.form_class(instance=obj, initial=initial_data)
|
||||||
obj = None
|
|
||||||
form = self.form_class(initial={k: request.GET.get(k) for k in self.fields_initial})
|
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'obj': obj,
|
'obj': obj,
|
||||||
@ -155,10 +159,10 @@ class ObjectEditView(View):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Validate object if editing an existing object
|
obj = self.get_object(kwargs)
|
||||||
obj = self.get_object(kwargs) if kwargs else None
|
obj = self.alter_obj(obj, args, kwargs)
|
||||||
|
|
||||||
form = self.form_class(request.POST, instance=obj)
|
form = self.form_class(request.POST, instance=obj)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
obj = form.save(commit=False)
|
obj = form.save(commit=False)
|
||||||
obj_created = not obj.pk
|
obj_created = not obj.pk
|
||||||
|
Loading…
Reference in New Issue
Block a user