mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 19:08:38 -06:00
Merge ffe374c54c
into 2567412121
This commit is contained in:
commit
290b5ff96e
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,3 +5,6 @@ configuration.py
|
|||||||
!upgrade.sh
|
!upgrade.sh
|
||||||
fabfile.py
|
fabfile.py
|
||||||
*.swp
|
*.swp
|
||||||
|
gunicorn_config.py
|
||||||
|
netbox/static/
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Provider, CircuitType, Circuit
|
from .models import Provider, CircuitType, Circuit, Termination
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Provider)
|
@admin.register(Provider)
|
||||||
@ -21,11 +21,21 @@ 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']
|
||||||
'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')
|
||||||
|
|
||||||
|
|
||||||
|
@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')
|
||||||
|
@ -49,13 +49,10 @@ class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
provider = ProviderNestedSerializer()
|
provider = ProviderNestedSerializer()
|
||||||
type = CircuitTypeNestedSerializer()
|
type = CircuitTypeNestedSerializer()
|
||||||
tenant = TenantNestedSerializer()
|
tenant = TenantNestedSerializer()
|
||||||
site = SiteNestedSerializer()
|
|
||||||
interface = InterfaceNestedSerializer()
|
|
||||||
|
|
||||||
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', '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
|
||||||
|
@ -14,12 +14,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)',
|
||||||
@ -75,26 +75,13 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (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:
|
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(pp_info__icontains=value) |
|
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@ -9,7 +10,7 @@ from utilities.forms import (
|
|||||||
SlugField, get_filter_choices,
|
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()
|
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):
|
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Provider
|
model = Provider
|
||||||
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug'))
|
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug'))
|
||||||
@ -97,8 +103,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
'cid', 'type', 'provider', 'tenant', 'install_date', 'comments'
|
||||||
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
'cid': "Unique circuit ID",
|
||||||
@ -165,8 +170,7 @@ class CircuitFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
|
fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
|
||||||
'commit_rate', 'xconnect_id', 'pp_info']
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
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'))
|
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'))
|
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'))
|
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
|
||||||
|
]
|
||||||
|
42
netbox/circuits/migrations/0006_auto_20160908_0213.py
Normal file
42
netbox/circuits/migrations/0006_auto_20160908_0213.py
Normal 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')]),
|
||||||
|
),
|
||||||
|
]
|
43
netbox/circuits/migrations/0007_auto_20160908_0220.py
Normal file
43
netbox/circuits/migrations/0007_auto_20160908_0220.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
20
netbox/circuits/migrations/0009_termination_comments.py
Normal file
20
netbox/circuits/migrations/0009_termination_comments.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -1,15 +1,13 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
|
||||||
from django.core.urlresolvers import reverse
|
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 dcim.models import Site, Interface
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
class Provider(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
stores information pertinent to the user's relationship with the Provider.
|
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')
|
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
|
||||||
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
|
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
|
||||||
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')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@ -61,7 +58,7 @@ class CircuitType(models.Model):
|
|||||||
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
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
|
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
|
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)
|
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)')
|
|
||||||
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')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['provider', 'cid']
|
ordering = ['provider', 'cid']
|
||||||
@ -95,6 +83,43 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return ','.join([
|
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.cid,
|
||||||
self.provider.name,
|
self.provider.name,
|
||||||
self.type.name,
|
self.type.name,
|
||||||
|
@ -56,12 +56,9 @@ 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')
|
termination_count = tables.Column(accessor=Accessor('count_terminations'),
|
||||||
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
verbose_name='Terminations')
|
||||||
verbose_name='Port Speed')
|
|
||||||
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('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', 'termination_count')
|
||||||
|
@ -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+)/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'),
|
||||||
|
|
||||||
|
# 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'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
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 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 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, CircuitType, Provider, Termination
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -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').annotate(count_terminations=Count('terminations'))
|
||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
@ -155,3 +159,64 @@ 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'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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}),
|
||||||
|
})
|
||||||
|
@ -20,7 +20,7 @@ class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
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):
|
class SiteNestedSerializer(SiteSerializer):
|
||||||
|
@ -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')
|
'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'])
|
||||||
|
@ -1051,7 +1051,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
# Initialize interface A choices
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
|
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 = [
|
self.fields['interface_a'].choices = [
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
(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
|
# Initialize interface_b choices if device_b is set
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
|
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'):
|
elif self.initial.get('device_b'):
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.initial['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:
|
else:
|
||||||
device_b_interfaces = []
|
device_b_interfaces = []
|
||||||
self.fields['interface_b'].choices = [
|
self.fields['interface_b'].choices = [
|
||||||
|
@ -304,7 +304,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def count_circuits(self):
|
def count_circuits(self):
|
||||||
return self.circuits.count()
|
return self.terminations.count()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1087,7 +1087,7 @@ class Interface(models.Model):
|
|||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
try:
|
try:
|
||||||
return bool(self.circuit)
|
return bool(self.termination)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
return bool(self.connection)
|
return bool(self.connection)
|
||||||
|
@ -26,7 +26,6 @@ class SiteTest(APITestCase):
|
|||||||
'count_vlans',
|
'count_vlans',
|
||||||
'count_racks',
|
'count_racks',
|
||||||
'count_devices',
|
'count_devices',
|
||||||
'count_circuits'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
nested_fields = [
|
nested_fields = [
|
||||||
|
@ -15,7 +15,7 @@ from django.utils.http import urlencode
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from ipam.models import Prefix, IPAddress, VLAN
|
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 extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
@ -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(),
|
'termination_count': Termination.objects.filter(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)
|
||||||
@ -554,9 +554,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', 'termination')
|
||||||
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', 'termination')
|
||||||
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')
|
||||||
|
@ -27,6 +27,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.circuits.change_circuit %}
|
{% 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">
|
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
Edit this circuit
|
Edit this circuit
|
||||||
@ -81,73 +85,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>
|
|
||||||
<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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@ -162,6 +99,41 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,16 +11,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 %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
@ -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">
|
||||||
|
49
netbox/templates/circuits/inc/_termination.html
Normal file
49
netbox/templates/circuits/inc/_termination.html
Normal 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>
|
@ -133,15 +133,6 @@
|
|||||||
<td>
|
<td>
|
||||||
<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>
|
|
||||||
<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>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
|
8
netbox/templates/circuits/termination_delete.html
Normal file
8
netbox/templates/circuits/termination_delete.html
Normal 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 %}
|
61
netbox/templates/circuits/termination_edit.html
Normal file
61
netbox/templates/circuits/termination_edit.html
Normal 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 & 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 %}
|
@ -24,9 +24,9 @@
|
|||||||
<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.termination %}
|
||||||
<td colspan="2">
|
<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>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
@ -35,7 +35,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.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 +56,8 @@
|
|||||||
<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.termination and perms.circuits.change_circuit %}
|
||||||
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit 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>
|
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -71,7 +71,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.termination.circuit %}
|
||||||
<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>
|
||||||
|
@ -151,8 +151,8 @@
|
|||||||
<p>VLANs</p>
|
<p>VLANs</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<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>
|
<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>Circuits</p>
|
<p>Terminations</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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]
|
return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Widgets
|
# Widgets
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user