Add Multipoint terminations to a circuit

This commit is contained in:
Nathan Gotz 2016-09-14 12:45:59 -05:00
parent 8341800a85
commit 36f615d1c4
24 changed files with 632 additions and 260 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ configuration.py
!upgrade.sh
fabfile.py
*.swp
*.bak
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,20 @@ 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

@ -13,17 +13,17 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
# site_id = django_filters.ModelMultipleChoiceFilter(
# name='circuits__site',
# queryset=Site.objects.all(),
# label='Site',
# )
# site = django_filters.ModelMultipleChoiceFilter(
# name='circuits__site',
# queryset=Site.objects.all(),
# to_field_name='slug',
# label='Site (slug)',
# )
class Meta:
model = Provider
@ -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

@ -9,7 +9,7 @@ from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
)
from .models import Circuit, CircuitType, Provider
from .models import Circuit, CircuitType, Provider, Termination
#
@ -85,40 +85,209 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
#
class CircuitForm(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'))
# 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 = 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'
]
# fields = [
# 'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
# 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
# ]
help_texts = {
'cid': "Unique circuit ID",
'install_date': "Format: YYYY-MM-DD",
'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)"
# '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)"
}
def __init__(self, *args, **kwargs):
super(CircuitForm, 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('circuit', '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('circuit', '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
# ]
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', 'install_date']
# 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.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
# port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
# commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
def circuit_type_choices():
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
def circuit_provider_choices():
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
def circuit_tenant_choices():
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
# def circuit_site_choices():
# site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
# return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
# widget=forms.SelectMultiple(attrs={'size': 8}))
#
# 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
@ -143,11 +312,11 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
# 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('circuit', 'connected_as_a', 'connected_as_b')
.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('circuit', 'connected_as_a', 'connected_as_b')
.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 = []
@ -157,64 +326,3 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) 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.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
def circuit_type_choices():
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
def circuit_provider_choices():
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
def circuit_tenant_choices():
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
def circuit_site_choices():
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

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,42 @@ 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,10 @@ 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,61 @@ 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

@ -1121,7 +1121,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
]
@ -1137,10 +1137,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

@ -269,7 +269,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
@property
def count_circuits(self):
return self.circuits.count()
return self.terminations.count()
#
@ -1052,7 +1052,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

@ -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

@ -42,3 +42,4 @@ urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]

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>