This commit is contained in:
Nathan Gotz 2016-09-15 16:39:54 +00:00 committed by GitHub
commit 290b5ff96e
29 changed files with 554 additions and 184 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ configuration.py
!upgrade.sh
fabfile.py
*.swp
gunicorn_config.py
netbox/static/

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Provider, CircuitType, Circuit
from .models import Provider, CircuitType, Circuit, Termination
@admin.register(Provider)
@ -21,11 +21,21 @@ class CircuitTypeAdmin(admin.ModelAdmin):
@admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date']
list_filter = ['provider', 'type', 'tenant']
exclude = ['interface']
def get_queryset(self, request):
qs = super(CircuitAdmin, self).get_queryset(request)
return qs.select_related('provider', 'type', 'tenant', 'site')
return qs.select_related('provider', 'type', 'tenant')
@admin.register(Termination)
class TerminationAdmin(admin.ModelAdmin):
list_display = ['tid', 'circuit', 'site', 'port_speed_human',
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
list_filter = ['site', 'tid', 'circuit']
exclude = ['interface']
def get_queryset(self, request):
qs = super(TerminationAdmin, self).get_queryset(request)
return qs.select_related('site', 'circuit')

View File

@ -49,13 +49,10 @@ class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer()
site = SiteNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta:
model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'custom_fields']
class CircuitNestedSerializer(CircuitSerializer):

View File

@ -43,7 +43,7 @@ class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
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')
serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter
@ -53,6 +53,6 @@ class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
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')
serializer_class = serializers.CircuitSerializer

View File

@ -14,12 +14,12 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
name='circuits__terminations__site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
name='circuits__terminations__site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@ -75,26 +75,13 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
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):
return queryset.filter(
Q(cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value) |
Q(comments__icontains=value)
)

View File

@ -1,4 +1,5 @@
from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@ -9,7 +10,7 @@ from utilities.forms import (
SlugField, get_filter_choices,
)
from .models import Circuit, CircuitType, Provider
from .models import Circuit, CircuitType, Provider, Termination
#
@ -57,6 +58,11 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
comments = CommentField()
def provider_site_choices():
site_choices = Site.objects.all()
return [(s.slug, s.name) for s in site_choices]
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug'))
@ -97,8 +103,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
'cid', 'type', 'provider', 'tenant', 'install_date', 'comments'
]
help_texts = {
'cid': "Unique circuit ID",
@ -165,8 +170,7 @@ class CircuitFromCSVForm(forms.ModelForm):
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
'commit_rate', 'xconnect_id', 'pp_info']
fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
class CircuitImportForm(BulkImportForm, BootstrapMixin):
@ -188,4 +192,108 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits'))
provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits'))
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits'))
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits'))
#
# Terminations
#
class TerminationForm(BootstrapMixin, CustomFieldForm):
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',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device'}
)
)
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?rack_id={{rack}}',
attrs={'filter-for': 'interface'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device_list',
field_to_update='device'
)
)
interface = forms.ModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Interface',
widget=APISelect(
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
disabled_indicator='is_connected'
)
)
comments = CommentField()
class Meta:
model = Termination
fields = [
'tid', 'site', 'rack', 'device', 'livesearch',
'interface', 'port_speed', 'upstream_speed', 'commit_rate',
'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
'tid': "Termination ID",
'port_speed': "Physical circuit speed",
'commit_rate': "Commited rate",
'xconnect_id': "ID of the local cross-connect",
'pp_info': "Patch panel ID and port number(s)"
}
widgets = {
'circuit': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super(TerminationForm, self).__init__(*args, **kwargs)
# If this circuit has been assigned to an interface, initialize rack and device
if self.instance.interface:
self.initial['rack'] = self.instance.interface.device.rack
self.initial['device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound:
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
elif self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
else:
self.fields['device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('device'):
interfaces = Interface.objects.filter(device=self.data['device'])\
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
elif self.initial.get('device'):
interfaces = Interface.objects.filter(device=self.initial['device'])\
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
else:
interfaces = []
self.fields['interface'].choices = [
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) for iface in interfaces
]

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-08 02:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0018_device_add_asset_tag'),
('circuits', '0005_circuit_add_upstream_speed'),
]
operations = [
migrations.CreateModel(
name='Termination',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('tid', models.CharField(max_length=50, verbose_name=b'Termination ID')),
('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)')),
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (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)')),
('comments', models.TextField(blank=True)),
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='terminations', to='circuits.Circuit')),
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='termination', to='dcim.Interface')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='terminations', to='dcim.Site')),
],
options={
'ordering': ['circuit', 'tid'],
},
),
migrations.AlterUniqueTogether(
name='termination',
unique_together=set([('circuit', 'tid')]),
),
]

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-08 02:20
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0006_auto_20160908_0213'),
]
operations = [
migrations.RemoveField(
model_name='circuit',
name='commit_rate',
),
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',
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-11 12:29
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0007_auto_20160908_0220'),
]
operations = [
migrations.RemoveField(
model_name='termination',
name='comments',
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-11 12:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0008_remove_termination_comments'),
]
operations = [
migrations.AddField(
model_name='termination',
name='comments',
field=models.TextField(blank=True),
),
]

View File

@ -1,15 +1,13 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models
from dcim.fields import ASNField
from dcim.models import Site, Interface
from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
class Provider(CreatedUpdatedModel, CustomFieldModel):
class Provider(CreatedUpdatedModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
@ -22,7 +20,6 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
class Meta:
ordering = ['name']
@ -61,7 +58,7 @@ class CircuitType(models.Model):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
class Circuit(CreatedUpdatedModel, CustomFieldModel):
class Circuit(CreatedUpdatedModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
@ -71,17 +68,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
provider = models.ForeignKey('Provider', 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)
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')
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)')
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)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
class Meta:
ordering = ['provider', 'cid']
@ -95,6 +83,43 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def to_csv(self):
return ','.join([
self.cid,
self.provider.name,
self.type.name,
self.tenant.name if self.tenant else '',
self.install_date.isoformat() if self.install_date else '',
])
class Termination(CreatedUpdatedModel):
"""
A Termination is where a site
"""
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.PROTECT)
tid = models.CharField(max_length=50, verbose_name='Termination ID')
site = models.ForeignKey(Site, related_name='terminations', on_delete=models.PROTECT)
interface = models.OneToOneField(Interface, related_name='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')
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)
class Meta:
ordering = ['circuit', 'tid']
unique_together = ['circuit', 'tid']
def __unicode__(self):
return u'{} Termination {}'.format(self.circuit.cid, self.tid)
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.circuit.pk])
def to_csv(self):
return ','.join([
self.tid,
self.cid,
self.provider.name,
self.type.name,

View File

@ -56,12 +56,9 @@ class CircuitTable(BaseTable):
type = tables.Column(verbose_name='Type')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
verbose_name='Port Speed')
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
verbose_name='Commit Rate')
termination_count = tables.Column(accessor=Accessor('count_terminations'),
verbose_name='Terminations')
class Meta(BaseTable.Meta):
model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate')
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_count')

View File

@ -31,4 +31,9 @@ urlpatterns = [
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'),
# Terminations
url(r'^circuits/(?P<pk>\d+)/terminations/add/$', views.termination_add, name='termination_add'),
url(r'^terminations/(?P<pk>\d+)/edit/$', views.TerminationEditView.as_view(), name='termination_edit'),
url(r'^terminations/(?P<pk>\d+)/delete/$', views.termination_delete, name='termination_delete'),
]

View File

@ -1,14 +1,18 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
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 django.contrib.auth.decorators import permission_required
from django.core.urlresolvers import reverse, resolve
from django.contrib import messages
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.forms import ConfirmationForm
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import Circuit, CircuitType, Provider
from .models import Circuit, CircuitType, Provider, Termination
#
@ -27,7 +31,7 @@ class ProviderListView(ObjectListView):
def provider(request, 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()
return render(request, 'circuits/provider.html', {
@ -103,7 +107,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').annotate(count_terminations=Count('terminations'))
filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm
table = tables.CircuitTable
@ -155,3 +159,64 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
cls = Circuit
default_redirect_url = 'circuits:circuit_list'
#
# Terminations
#
@permission_required('circuits.change_circuit')
def termination_add(request, pk):
circuit = get_object_or_404(Circuit, pk=pk)
if request.method == 'POST':
form = forms.TerminationForm(request.POST)
if form.is_valid():
if not form.errors:
new_termination = form.save(commit=False)
new_termination.circuit = circuit
new_termination.save()
if '_addanother' in request.POST:
return redirect('circuits:termination_add', pk=circuit.pk)
else:
return redirect('circuits:circuit', pk=circuit.pk)
else:
form = forms.TerminationForm()
return render(request, 'circuits/termination_edit.html', {
'form': form,
'obj_type': "Termination",
'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}),
})
class TerminationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit'
model = Termination
form_class = forms.TerminationForm
fields_initial = ['site']
template_name = 'circuits/termination_edit.html'
cancel_url = 'circuits:circuit_list'
@permission_required('circuits.delete_circuit')
def termination_delete(request, pk):
termination = get_object_or_404(Termination, pk=pk)
if request.method == 'POST':
form = ConfirmationForm(request.POST)
if form.is_valid():
termination.delete()
messages.success(request, "Termination {0} has been deleted from {1}".format(termination, termination.circuit))
return redirect('circuits:circuit', pk=termination.circuit.pk)
else:
form = ConfirmationForm()
return render(request, 'circuits/termination_delete.html', {
'termination': termination,
'form': form,
'cancel_url': reverse('circuits:circuit', kwargs={'pk': termination.circuit.pk}),
})

View File

@ -20,7 +20,7 @@ class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
class Meta:
model = Site
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices']
class SiteNestedSerializer(SiteSerializer):

View File

@ -484,7 +484,7 @@ class RelatedConnectionsView(APIView):
# Interface connections
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
'circuit')
'termination')
for iface in interfaces:
data = serializers.InterfaceDetailSerializer(instance=iface).data
del(data['device'])

View File

@ -1051,7 +1051,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
# Initialize interface A choices
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
.select_related('circuit', 'connected_as_a', 'connected_as_b')
.select_related('termination', 'connected_as_a', 'connected_as_b')
self.fields['interface_a'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
]
@ -1067,10 +1067,10 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
# Initialize interface_b choices if device_b is set
if self.is_bound:
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
elif self.initial.get('device_b'):
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
else:
device_b_interfaces = []
self.fields['interface_b'].choices = [

View File

@ -304,7 +304,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
@property
def count_circuits(self):
return self.circuits.count()
return self.terminations.count()
#
@ -1087,7 +1087,7 @@ class Interface(models.Model):
@property
def is_connected(self):
try:
return bool(self.circuit)
return bool(self.termination)
except ObjectDoesNotExist:
pass
return bool(self.connection)

View File

@ -26,7 +26,6 @@ class SiteTest(APITestCase):
'count_vlans',
'count_racks',
'count_devices',
'count_circuits'
]
nested_fields = [

View File

@ -15,7 +15,7 @@ from django.utils.http import urlencode
from django.views.generic import View
from ipam.models import Prefix, IPAddress, VLAN
from circuits.models import Circuit
from circuits.models import Circuit, Termination
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm
from utilities.views import (
@ -78,7 +78,7 @@ def site(request, slug):
'device_count': Device.objects.filter(rack__site=site).count(),
'prefix_count': Prefix.objects.filter(site=site).count(),
'vlan_count': VLAN.objects.filter(site=site).count(),
'circuit_count': Circuit.objects.filter(site=site).count(),
'termination_count': Termination.objects.filter(site=site).count(),
}
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site)
@ -554,9 +554,9 @@ def device(request, pk):
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
)
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', 'termination')
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', 'termination')
device_bays = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter('name')

View File

@ -27,6 +27,10 @@
</div>
<div class="pull-right">
{% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:termination_add' pk=circuit.pk %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add termination
</a>
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this circuit
@ -81,73 +85,6 @@
{% endif %}
</td>
</tr>
<tr>
<td>Speed</td>
<td>
{% if circuit.upstream_speed %}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
{% else %}
{{ circuit.port_speed_human }}
{% endif %}
</td>
</tr>
<tr>
<td>Commit Rate</td>
<td>
{% if circuit.commit_rate %}
{{ circuit.commit_rate_human }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'inc/created_updated.html' with obj=circuit %}
</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>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">
@ -162,6 +99,41 @@
{% endif %}
</div>
</div>
{% include 'inc/created_updated.html' with obj=circuit %}
</div>
<div class="col-md-6">
{% for t in circuit.terminations.all %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="pull-right">
{% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:termination_edit' pk=t.pk %}" class="btn btn-info btn-xs" title="Edit interface">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.circuits.delete_circuit %}
<a href="{% url 'circuits:termination_delete' pk=t.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
</span>
<strong>Termination ID {{t.tid }}</strong>
</div>
<table class="table table-hover panel-body">
{% include 'circuits/inc/_termination.html' %}
</table>
</div>
{% empty %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Termination</strong>
</div>
<div class="text-muted" colspan="7">
None
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -11,16 +11,6 @@
{% render_field form.type %}
{% render_field form.tenant %}
{% 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 %}
</div>
</div>
{% if form.custom_fields %}
@ -31,26 +21,6 @@
</div>
</div>
{% 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-heading"><strong>Comments</strong></div>
<div class="panel-body">

View File

@ -0,0 +1,49 @@
{% load helpers %}
<tr>
<th>Site</th>
<td>
<a href="{% url 'dcim:site' slug=t.site.slug %}">{{ t.site }}</a>
</td>
</tr>
<tr>
<th>Termination</th>
<td>
{% if t.interface %}
<span><a href="{% url 'dcim:device' pk=t.interface.device.pk %}">{{ t.interface.device }}</a> {{ t.interface }}</span>
{% else %}
<span class="text-muted">Not defined</span>
{% endif %}
</td>
</tr>
<tr>
<th>Cross-Connect</th>
<td>
{% if t.xconnect_id %}
{{ t.xconnect_id }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<th>Patch Panel/Port</th>
<td>
{% if t.pp_info %}
{{ t.pp_info }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<th colspan="2">Comments</th>
</tr>
<tr>
{% if t.comments %}
<td colspan="2">
{{ t.comments|gfm }}
</td>
{% else %}
<td class="text-muted" colspan="2">None</td>
{% endif %}
</tr>

View File

@ -133,15 +133,6 @@
<td>
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
</td>
<td>
<a href="{% url 'dcim:site' slug=c.site.slug %}">{{ c.site }}</a>
</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>
{% empty %}
<tr>

View File

@ -0,0 +1,8 @@
{% extends 'utilities/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete termination {{ termination }}?{% endblock %}
{% block message %}
<p>Are you sure you want to delete this termination from <strong>{{ termination.circuit.cid }}</strong>?</p>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends 'utilities/obj_edit.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination</strong></div>
<div class="panel-body">
{{ form.circuit }}
{% render_field form.tid %}
{% 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 %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Device &amp; Interface</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-heading"><strong>Comments</strong></div>
<div class="panel-body">
{% render_field form.comments %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}"></script>
{% endblock %}

View File

@ -24,9 +24,9 @@
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
</td>
{% endwith %}
{% elif iface.circuit %}
{% elif iface.termination %}
<td colspan="2">
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a>
<a href="{% url 'circuits:circuit' pk=iface.termination.circuit.pk %}">{{ iface.termination.circuit }}</a>
</td>
{% else %}
<td colspan="2">
@ -35,7 +35,7 @@
{% endif %}
<td class="text-right">
{% if show_graphs %}
{% if iface.circuit or iface.connection %}
{% if iface.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">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button>
@ -56,8 +56,8 @@
<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>
</a>
{% elif iface.circuit and perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
{% elif iface.termination and perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=iface.termination.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a>
{% else %}
@ -71,7 +71,7 @@
</a>
{% endif %}
{% if perms.dcim.delete_interface %}
{% if iface.connection or iface.circuit %}
{% if iface.connection or iface.termination.circuit %}
<button class="btn btn-danger btn-xs" disabled="disabled">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>

View File

@ -151,8 +151,8 @@
<p>VLANs</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
<p>Circuits</p>
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.termination_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.termination_count }}</a></h2>
<p>Terminations</p>
</div>
</div>
</div>

View File

@ -55,7 +55,6 @@ def get_filter_choices(model, id_field='pk', select_related=[], count_field=None
return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
#
# Widgets
#