diff --git a/.gitignore b/.gitignore index 954607b60..0407ad91a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ configuration.py !upgrade.sh fabfile.py *.swp +gunicorn_config.py +netbox/static/ + diff --git a/netbox/circuits/admin.py b/netbox/circuits/admin.py index 97711b7a8..3e730682f 100644 --- a/netbox/circuits/admin.py +++ b/netbox/circuits/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Provider, CircuitType, Circuit +from .models import Provider, CircuitType, Circuit, Termination @admin.register(Provider) @@ -21,11 +21,21 @@ class CircuitTypeAdmin(admin.ModelAdmin): @admin.register(Circuit) class CircuitAdmin(admin.ModelAdmin): - list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human', - 'upstream_speed_human', 'commit_rate_human', 'xconnect_id'] + list_display = ['cid', 'provider', 'type', 'tenant', 'install_date'] list_filter = ['provider', 'type', 'tenant'] - exclude = ['interface'] def get_queryset(self, request): qs = super(CircuitAdmin, self).get_queryset(request) - return qs.select_related('provider', 'type', 'tenant', 'site') + return qs.select_related('provider', 'type', 'tenant') + + +@admin.register(Termination) +class TerminationAdmin(admin.ModelAdmin): + list_display = ['tid', 'circuit', 'site', 'port_speed_human', + 'upstream_speed_human', 'commit_rate_human', 'xconnect_id'] + list_filter = ['site', 'tid', 'circuit'] + exclude = ['interface'] + + def get_queryset(self, request): + qs = super(TerminationAdmin, self).get_queryset(request) + return qs.select_related('site', 'circuit') diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index d7f32f958..87030322a 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -49,13 +49,10 @@ class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer): provider = ProviderNestedSerializer() type = CircuitTypeNestedSerializer() tenant = TenantNestedSerializer() - site = SiteNestedSerializer() - interface = InterfaceNestedSerializer() class Meta: model = Circuit - fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed', - 'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields'] + fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'custom_fields'] class CircuitNestedSerializer(CircuitSerializer): diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 866f9283b..d89286036 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -43,7 +43,7 @@ class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView): """ List circuits (filterable) """ - queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\ + queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\ .prefetch_related('custom_field_values__field') serializer_class = serializers.CircuitSerializer filter_class = CircuitFilter @@ -53,6 +53,6 @@ class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single circuit """ - queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\ + queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\ .prefetch_related('custom_field_values__field') serializer_class = serializers.CircuitSerializer diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f719290ed..b9f37acad 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -14,12 +14,12 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Search', ) site_id = django_filters.ModelMultipleChoiceFilter( - name='circuits__site', + name='circuits__terminations__site', queryset=Site.objects.all(), label='Site', ) site = django_filters.ModelMultipleChoiceFilter( - name='circuits__site', + name='circuits__terminations__site', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', @@ -75,26 +75,13 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) - site_id = django_filters.ModelMultipleChoiceFilter( - name='site', - queryset=Site.objects.all(), - label='Site (ID)', - ) - site = django_filters.ModelMultipleChoiceFilter( - name='site', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site (slug)', - ) class Meta: model = Circuit - fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date'] + fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'install_date'] def search(self, queryset, value): return queryset.filter( Q(cid__icontains=value) | - Q(xconnect_id__icontains=value) | - Q(pp_info__icontains=value) | Q(comments__icontains=value) ) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index c934ab630..1742d8828 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.db.models import Count from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm @@ -9,7 +10,7 @@ from utilities.forms import ( SlugField, get_filter_choices, ) -from .models import Circuit, CircuitType, Provider +from .models import Circuit, CircuitType, Provider, Termination # @@ -57,6 +58,11 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): comments = CommentField() +def provider_site_choices(): + site_choices = Site.objects.all() + return [(s.slug, s.name) for s in site_choices] + + class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Provider site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug')) @@ -97,8 +103,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm): class Meta: model = Circuit fields = [ - 'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date', - 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments' + 'cid', 'type', 'provider', 'tenant', 'install_date', 'comments' ] help_texts = { 'cid': "Unique circuit ID", @@ -165,8 +170,7 @@ class CircuitFromCSVForm(forms.ModelForm): class Meta: model = Circuit - fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed', - 'commit_rate', 'xconnect_id', 'pp_info'] + fields = ['cid', 'provider', 'type', 'tenant', 'install_date'] class CircuitImportForm(BulkImportForm, BootstrapMixin): @@ -188,4 +192,108 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits')) provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits')) tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits')) - site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits')) + + +# +# Terminations +# +class TerminationForm(BootstrapMixin, CustomFieldForm): + site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) + rack = forms.ModelChoiceField( + queryset=Rack.objects.all(), + required=False, + label='Rack', + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', + attrs={'filter-for': 'device'} + ) + ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + label='Device', + widget=APISelect( + api_url='/api/dcim/devices/?rack_id={{rack}}', + attrs={'filter-for': 'interface'} + ) + ) + livesearch = forms.CharField( + required=False, + label='Device', + widget=Livesearch( + query_key='q', + query_url='dcim-api:device_list', + field_to_update='device' + ) + ) + interface = forms.ModelChoiceField( + queryset=Interface.objects.all(), + required=False, + label='Interface', + widget=APISelect( + api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical', + disabled_indicator='is_connected' + ) + ) + comments = CommentField() + + class Meta: + model = Termination + fields = [ + 'tid', 'site', 'rack', 'device', 'livesearch', + 'interface', 'port_speed', 'upstream_speed', 'commit_rate', + 'xconnect_id', 'pp_info', 'comments' + ] + help_texts = { + 'tid': "Termination ID", + 'port_speed': "Physical circuit speed", + 'commit_rate': "Commited rate", + 'xconnect_id': "ID of the local cross-connect", + 'pp_info': "Patch panel ID and port number(s)" + } + widgets = { + 'circuit': forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + + super(TerminationForm, self).__init__(*args, **kwargs) + + # If this circuit has been assigned to an interface, initialize rack and device + if self.instance.interface: + self.initial['rack'] = self.instance.interface.device.rack + self.initial['device'] = self.instance.interface.device + + # Limit rack choices + if self.is_bound: + self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site']) + elif self.initial.get('site'): + self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site']) + else: + self.fields['rack'].choices = [] + + # Limit device choices + if self.is_bound and self.data.get('rack'): + self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack']) + elif self.initial.get('rack'): + self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack']) + else: + self.fields['device'].choices = [] + + # Limit interface choices + if self.is_bound and self.data.get('device'): + interfaces = Interface.objects.filter(device=self.data['device'])\ + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') + self.fields['interface'].widget.attrs['initial'] = self.data.get('interface') + elif self.initial.get('device'): + interfaces = Interface.objects.filter(device=self.initial['device'])\ + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') + self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface') + else: + interfaces = [] + self.fields['interface'].choices = [ + (iface.id, { + 'label': iface.name, + 'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'), + }) for iface in interfaces + ] diff --git a/netbox/circuits/migrations/0006_auto_20160908_0213.py b/netbox/circuits/migrations/0006_auto_20160908_0213.py new file mode 100644 index 000000000..c34871935 --- /dev/null +++ b/netbox/circuits/migrations/0006_auto_20160908_0213.py @@ -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')]), + ), + ] diff --git a/netbox/circuits/migrations/0007_auto_20160908_0220.py b/netbox/circuits/migrations/0007_auto_20160908_0220.py new file mode 100644 index 000000000..bea1753bf --- /dev/null +++ b/netbox/circuits/migrations/0007_auto_20160908_0220.py @@ -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', + ), + ] diff --git a/netbox/circuits/migrations/0008_remove_termination_comments.py b/netbox/circuits/migrations/0008_remove_termination_comments.py new file mode 100644 index 000000000..08252f38e --- /dev/null +++ b/netbox/circuits/migrations/0008_remove_termination_comments.py @@ -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', + ), + ] diff --git a/netbox/circuits/migrations/0009_termination_comments.py b/netbox/circuits/migrations/0009_termination_comments.py new file mode 100644 index 000000000..b9541db55 --- /dev/null +++ b/netbox/circuits/migrations/0009_termination_comments.py @@ -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), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index fd4fdf634..603e3b9e9 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,15 +1,13 @@ -from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.db import models from dcim.fields import ASNField from dcim.models import Site, Interface -from extras.models import CustomFieldModel, CustomFieldValue from tenancy.models import Tenant from utilities.models import CreatedUpdatedModel -class Provider(CreatedUpdatedModel, CustomFieldModel): +class Provider(CreatedUpdatedModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model stores information pertinent to the user's relationship with the Provider. @@ -22,7 +20,6 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): noc_contact = models.TextField(blank=True, verbose_name='NOC contact') admin_contact = models.TextField(blank=True, verbose_name='Admin contact') comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['name'] @@ -61,7 +58,7 @@ class CircuitType(models.Model): return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) -class Circuit(CreatedUpdatedModel, CustomFieldModel): +class Circuit(CreatedUpdatedModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device @@ -71,17 +68,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT) type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT) tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT) - site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT) - interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True) install_date = models.DateField(blank=True, null=True, verbose_name='Date installed') - port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)') - upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)', - help_text='Upstream speed, if different from port speed') - commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)') - xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') - pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['provider', 'cid'] @@ -95,6 +83,43 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): def to_csv(self): return ','.join([ + self.cid, + self.provider.name, + self.type.name, + self.tenant.name if self.tenant else '', + self.install_date.isoformat() if self.install_date else '', + ]) + + +class Termination(CreatedUpdatedModel): + """ + A Termination is where a site + """ + circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.PROTECT) + tid = models.CharField(max_length=50, verbose_name='Termination ID') + site = models.ForeignKey(Site, related_name='terminations', on_delete=models.PROTECT) + interface = models.OneToOneField(Interface, related_name='termination', blank=True, null=True) + port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)') + upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)', + help_text='Upstream speed, if different from port speed') + commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)') + xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') + pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') + comments = models.TextField(blank=True) + + class Meta: + ordering = ['circuit', 'tid'] + unique_together = ['circuit', 'tid'] + + def __unicode__(self): + return u'{} Termination {}'.format(self.circuit.cid, self.tid) + + def get_absolute_url(self): + return reverse('circuits:circuit', args=[self.circuit.pk]) + + def to_csv(self): + return ','.join([ + self.tid, self.cid, self.provider.name, self.type.name, diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index f82459890..761583e16 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -56,12 +56,9 @@ class CircuitTable(BaseTable): type = tables.Column(verbose_name='Type') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'), - verbose_name='Port Speed') - commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'), - verbose_name='Commit Rate') + termination_count = tables.Column(accessor=Accessor('count_terminations'), + verbose_name='Terminations') class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate') + fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_count') diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 9ecb1d5ae..f3e7eefa8 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -31,4 +31,9 @@ urlpatterns = [ url(r'^circuits/(?P\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), + # Terminations + url(r'^circuits/(?P\d+)/terminations/add/$', views.termination_add, name='termination_add'), + url(r'^terminations/(?P\d+)/edit/$', views.TerminationEditView.as_view(), name='termination_edit'), + url(r'^terminations/(?P\d+)/delete/$', views.termination_delete, name='termination_delete'), + ] diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 28c4d6844..08a468821 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,14 +1,18 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, render +from django.contrib.auth.decorators import permission_required +from django.core.urlresolvers import reverse, resolve +from django.contrib import messages from extras.models import Graph, GRAPH_TYPE_PROVIDER +from utilities.forms import ConfirmationForm from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables -from .models import Circuit, CircuitType, Provider +from .models import Circuit, CircuitType, Provider, Termination # @@ -27,7 +31,7 @@ class ProviderListView(ObjectListView): def provider(request, slug): provider = get_object_or_404(Provider, slug=slug) - circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device') + circuits = Circuit.objects.filter(provider=provider) show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() return render(request, 'circuits/provider.html', { @@ -103,7 +107,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class CircuitListView(ObjectListView): - queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site') + queryset = Circuit.objects.select_related('provider', 'type', 'tenant').annotate(count_terminations=Count('terminations')) filter = filters.CircuitFilter filter_form = forms.CircuitFilterForm table = tables.CircuitTable @@ -155,3 +159,64 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuit' cls = Circuit default_redirect_url = 'circuits:circuit_list' + + +# +# Terminations +# +@permission_required('circuits.change_circuit') +def termination_add(request, pk): + + circuit = get_object_or_404(Circuit, pk=pk) + + if request.method == 'POST': + form = forms.TerminationForm(request.POST) + if form.is_valid(): + if not form.errors: + new_termination = form.save(commit=False) + new_termination.circuit = circuit + new_termination.save() + if '_addanother' in request.POST: + return redirect('circuits:termination_add', pk=circuit.pk) + else: + return redirect('circuits:circuit', pk=circuit.pk) + + else: + form = forms.TerminationForm() + + return render(request, 'circuits/termination_edit.html', { + 'form': form, + 'obj_type': "Termination", + 'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}), + }) + + +class TerminationEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.change_circuit' + model = Termination + form_class = forms.TerminationForm + fields_initial = ['site'] + template_name = 'circuits/termination_edit.html' + cancel_url = 'circuits:circuit_list' + + +@permission_required('circuits.delete_circuit') +def termination_delete(request, pk): + + termination = get_object_or_404(Termination, pk=pk) + + if request.method == 'POST': + form = ConfirmationForm(request.POST) + if form.is_valid(): + termination.delete() + messages.success(request, "Termination {0} has been deleted from {1}".format(termination, termination.circuit)) + return redirect('circuits:circuit', pk=termination.circuit.pk) + + else: + form = ConfirmationForm() + + return render(request, 'circuits/termination_delete.html', { + 'termination': termination, + 'form': form, + 'cancel_url': reverse('circuits:circuit', kwargs={'pk': termination.circuit.pk}), + }) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 583f63d8b..8aa330c35 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -20,7 +20,7 @@ class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer): class Meta: model = Site fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments', - 'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits'] + 'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices'] class SiteNestedSerializer(SiteSerializer): diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 01a8c6f61..9b49d9cec 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -484,7 +484,7 @@ class RelatedConnectionsView(APIView): # Interface connections interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b', - 'circuit') + 'termination') for iface in interfaces: data = serializers.InterfaceDetailSerializer(instance=iface).data del(data['device']) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7625955b4..e0a7a51d7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1051,7 +1051,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin): # Initialize interface A choices device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \ - .select_related('circuit', 'connected_as_a', 'connected_as_b') + .select_related('termination', 'connected_as_a', 'connected_as_b') self.fields['interface_a'].choices = [ (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces ] @@ -1067,10 +1067,10 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin): # Initialize interface_b choices if device_b is set if self.is_bound: device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') elif self.initial.get('device_b'): device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') else: device_b_interfaces = [] self.fields['interface_b'].choices = [ diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 5627ebde1..7855ed35c 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -304,7 +304,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): @property def count_circuits(self): - return self.circuits.count() + return self.terminations.count() # @@ -1087,7 +1087,7 @@ class Interface(models.Model): @property def is_connected(self): try: - return bool(self.circuit) + return bool(self.termination) except ObjectDoesNotExist: pass return bool(self.connection) diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index f7f63c555..fdef5c703 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -26,7 +26,6 @@ class SiteTest(APITestCase): 'count_vlans', 'count_racks', 'count_devices', - 'count_circuits' ] nested_fields = [ diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f3b2f4bf1..354b405eb 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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') diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 8844dc43a..7f2735634 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -27,6 +27,10 @@ - {% include 'inc/created_updated.html' with obj=circuit %} - -
-
-
- Termination -
- - - - - - - - - - - - - - - - -
Site - {{ circuit.site }} -
Termination - {% if circuit.interface %} - {{ circuit.interface.device }} {{ circuit.interface }} - {% else %} - Not defined - {% endif %} -
Cross-Connect - {% if circuit.xconnect_id %} - {{ circuit.xconnect_id }} - {% else %} - N/A - {% endif %} -
Patch Panel/Port - {% if circuit.pp_info %} - {{ circuit.pp_info }} - {% else %} - N/A - {% endif %} -
@@ -162,6 +99,41 @@ {% endif %}
+ + {% include 'inc/created_updated.html' with obj=circuit %} + +
+ {% for t in circuit.terminations.all %} +
+
+ + {% if perms.circuits.change_circuit %} + + + + {% endif %} + {% if perms.circuits.delete_circuit %} + + + + {% endif %} + + Termination ID {{t.tid }} +
+ + {% include 'circuits/inc/_termination.html' %} +
+
+ {% empty %} +
+
+ Termination +
+
+ None +
+
+ {% endfor %}
{% endblock %} diff --git a/netbox/templates/circuits/circuit_edit.html b/netbox/templates/circuits/circuit_edit.html index 863b0a0a2..30f9353de 100644 --- a/netbox/templates/circuits/circuit_edit.html +++ b/netbox/templates/circuits/circuit_edit.html @@ -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 %} - - -
-
Bandwidth
-
- {% render_field form.port_speed %} - {% render_field form.upstream_speed %} - {% render_field form.commit_rate %}
{% if form.custom_fields %} @@ -31,26 +21,6 @@ {% endif %} -
-
Termination
-
- {% render_field form.site %} - -
-
- {% render_field form.rack %} - {% render_field form.device %} -
- -
- {% render_field form.interface %} -
-
Comments
diff --git a/netbox/templates/circuits/inc/_termination.html b/netbox/templates/circuits/inc/_termination.html new file mode 100644 index 000000000..577af8472 --- /dev/null +++ b/netbox/templates/circuits/inc/_termination.html @@ -0,0 +1,49 @@ +{% load helpers %} + + Site + + {{ t.site }} + + + + Termination + + {% if t.interface %} + {{ t.interface.device }} {{ t.interface }} + {% else %} + Not defined + {% endif %} + + + + Cross-Connect + + {% if t.xconnect_id %} + {{ t.xconnect_id }} + {% else %} + N/A + {% endif %} + + + + Patch Panel/Port + + {% if t.pp_info %} + {{ t.pp_info }} + {% else %} + N/A + {% endif %} + + + + Comments + + + {% if t.comments %} + + {{ t.comments|gfm }} + + {% else %} + None + {% endif %} + diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 742dbcc3e..e8e72d60f 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -133,15 +133,6 @@ {{ c.cid }} - - {{ c.site }} - - - {% if c.interface %} - {{ c.interface.device }} - {% endif %} - - {{ c.port_speed_human }} {% empty %} diff --git a/netbox/templates/circuits/termination_delete.html b/netbox/templates/circuits/termination_delete.html new file mode 100644 index 000000000..3ec1bc87c --- /dev/null +++ b/netbox/templates/circuits/termination_delete.html @@ -0,0 +1,8 @@ +{% extends 'utilities/confirmation_form.html' %} +{% load form_helpers %} + +{% block title %}Delete termination {{ termination }}?{% endblock %} + +{% block message %} +

Are you sure you want to delete this termination from {{ termination.circuit.cid }}?

+{% endblock %} diff --git a/netbox/templates/circuits/termination_edit.html b/netbox/templates/circuits/termination_edit.html new file mode 100644 index 000000000..ab582aaf5 --- /dev/null +++ b/netbox/templates/circuits/termination_edit.html @@ -0,0 +1,61 @@ +{% extends 'utilities/obj_edit.html' %} +{% load static from staticfiles %} +{% load form_helpers %} + +{% block form %} +
+
Termination
+
+ {{ form.circuit }} + {% render_field form.tid %} + {% render_field form.xconnect_id %} + {% render_field form.pp_info %} +
+
+
+
Bandwidth
+
+ {% render_field form.port_speed %} + {% render_field form.upstream_speed %} + {% render_field form.commit_rate %} +
+
+ {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} +
+
Device & Interface
+
+ {% render_field form.site %} + +
+
+ {% render_field form.rack %} + {% render_field form.device %} +
+ +
+ {% render_field form.interface %} +
+
+
+
Comments
+
+ {% render_field form.comments %} +
+
+{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/dcim/inc/_interface.html b/netbox/templates/dcim/inc/_interface.html index a48200c30..b34711cc3 100644 --- a/netbox/templates/dcim/inc/_interface.html +++ b/netbox/templates/dcim/inc/_interface.html @@ -24,9 +24,9 @@ {{ connected_iface }} {% endwith %} - {% elif iface.circuit %} + {% elif iface.termination %} - {{ iface.circuit }} + {{ iface.termination.circuit }} {% else %} @@ -35,7 +35,7 @@ {% endif %} {% if show_graphs %} - {% if iface.circuit or iface.connection %} + {% if iface.termination or iface.connection %} @@ -56,8 +56,8 @@ - {% elif iface.circuit and perms.circuits.change_circuit %} - + {% elif iface.termination and perms.circuits.change_circuit %} + {% else %} @@ -71,7 +71,7 @@ {% endif %} {% if perms.dcim.delete_interface %} - {% if iface.connection or iface.circuit %} + {% if iface.connection or iface.termination.circuit %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index ffcf382ab..ea4269b24 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -151,8 +151,8 @@

VLANs

diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 30989ddcf..315ec509a 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -55,7 +55,6 @@ def get_filter_choices(model, id_field='pk', select_related=[], count_field=None return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset] - # # Widgets #