From 36f615d1c4e1ab103ca13d517731b98a7130a5f0 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 12:45:59 -0500 Subject: [PATCH 1/9] Add Multipoint terminations to a circuit --- .gitignore | 3 + netbox/circuits/admin.py | 19 +- netbox/circuits/filters.py | 37 +-- netbox/circuits/forms.py | 274 ++++++++++++------ .../migrations/0006_auto_20160908_0213.py | 42 +++ .../migrations/0007_auto_20160908_0220.py | 43 +++ .../0008_remove_termination_comments.py | 19 ++ .../migrations/0009_termination_comments.py | 20 ++ netbox/circuits/models.py | 52 +++- netbox/circuits/tables.py | 10 +- netbox/circuits/urls.py | 5 + netbox/circuits/views.py | 70 ++++- netbox/dcim/forms.py | 6 +- netbox/dcim/models.py | 4 +- netbox/dcim/views.py | 8 +- netbox/netbox/urls.py | 1 + netbox/templates/circuits/circuit.html | 106 +++---- netbox/templates/circuits/circuit_edit.html | 30 -- .../templates/circuits/inc/_termination.html | 49 ++++ netbox/templates/circuits/provider.html | 9 - .../circuits/termination_delete.html | 8 + .../templates/circuits/termination_edit.html | 61 ++++ netbox/templates/dcim/inc/_interface.html | 12 +- netbox/templates/dcim/site.html | 4 +- 24 files changed, 632 insertions(+), 260 deletions(-) create mode 100644 netbox/circuits/migrations/0006_auto_20160908_0213.py create mode 100644 netbox/circuits/migrations/0007_auto_20160908_0220.py create mode 100644 netbox/circuits/migrations/0008_remove_termination_comments.py create mode 100644 netbox/circuits/migrations/0009_termination_comments.py create mode 100644 netbox/templates/circuits/inc/_termination.html create mode 100644 netbox/templates/circuits/termination_delete.html create mode 100644 netbox/templates/circuits/termination_edit.html diff --git a/.gitignore b/.gitignore index 954607b60..de6c25552 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ configuration.py !upgrade.sh fabfile.py *.swp +*.bak +gunicorn_config.py +netbox/static diff --git a/netbox/circuits/admin.py b/netbox/circuits/admin.py index 97711b7a8..8a194a6e6 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,20 @@ class CircuitTypeAdmin(admin.ModelAdmin): @admin.register(Circuit) class CircuitAdmin(admin.ModelAdmin): - list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human', - 'upstream_speed_human', 'commit_rate_human', 'xconnect_id'] + list_display = ['cid', 'provider', 'type', 'tenant','install_date'] list_filter = ['provider', 'type', 'tenant'] - exclude = ['interface'] def get_queryset(self, request): qs = super(CircuitAdmin, self).get_queryset(request) - return qs.select_related('provider', 'type', 'tenant', 'site') + return qs.select_related('provider', 'type', 'tenant') + +@admin.register(Termination) +class TerminationAdmin(admin.ModelAdmin): + list_display = ['tid', 'circuit', 'site', 'port_speed_human', + 'upstream_speed_human', 'commit_rate_human', 'xconnect_id'] + list_filter = ['site', 'tid', 'circuit'] + exclude = ['interface'] + + def get_queryset(self, request): + qs = super(TerminationAdmin, self).get_queryset(request) + return qs.select_related('site', 'circuit') diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f719290ed..0d76308c6 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -13,17 +13,17 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search', label='Search', ) - site_id = django_filters.ModelMultipleChoiceFilter( - name='circuits__site', - queryset=Site.objects.all(), - label='Site', - ) - site = django_filters.ModelMultipleChoiceFilter( - name='circuits__site', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site (slug)', - ) + # site_id = django_filters.ModelMultipleChoiceFilter( + # name='circuits__site', + # queryset=Site.objects.all(), + # label='Site', + # ) + # site = django_filters.ModelMultipleChoiceFilter( + # name='circuits__site', + # queryset=Site.objects.all(), + # to_field_name='slug', + # label='Site (slug)', + # ) class Meta: model = Provider @@ -75,26 +75,13 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) - site_id = django_filters.ModelMultipleChoiceFilter( - name='site', - queryset=Site.objects.all(), - label='Site (ID)', - ) - site = django_filters.ModelMultipleChoiceFilter( - name='site', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site (slug)', - ) class Meta: model = Circuit - fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date'] + fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'install_date'] def search(self, queryset, value): return queryset.filter( Q(cid__icontains=value) | - Q(xconnect_id__icontains=value) | - Q(pp_info__icontains=value) | Q(comments__icontains=value) ) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 0cb7a3016..9c389fe41 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -9,7 +9,7 @@ from utilities.forms import ( APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField, ) -from .models import Circuit, CircuitType, Provider +from .models import Circuit, CircuitType, Provider, Termination # @@ -85,40 +85,209 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin): # class CircuitForm(BootstrapMixin, CustomFieldForm): - site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) - rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack', - widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', - attrs={'filter-for': 'device'})) - device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', - attrs={'filter-for': 'interface'})) - livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( - query_key='q', query_url='dcim-api:device_list', field_to_update='device') - ) - interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface', - widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical', - disabled_indicator='is_connected')) + # site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) + # rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack', + # widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', + # attrs={'filter-for': 'device'})) + # device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', + # widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', + # attrs={'filter-for': 'interface'})) + # livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( + # query_key='q', query_url='dcim-api:device_list', field_to_update='device') + # ) + # interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface', + # widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical', + # disabled_indicator='is_connected')) comments = CommentField() class Meta: model = Circuit fields = [ - 'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date', - 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments' + 'cid', 'type', 'provider', 'tenant', 'install_date', 'comments' ] + # fields = [ + # 'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date', + # 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments' + # ] help_texts = { 'cid': "Unique circuit ID", 'install_date': "Format: YYYY-MM-DD", - 'port_speed': "Physical circuit speed", - 'commit_rate': "Commited rate", - 'xconnect_id': "ID of the local cross-connect", - 'pp_info': "Patch panel ID and port number(s)" + # 'port_speed': "Physical circuit speed", + # 'commit_rate': "Commited rate", + # 'xconnect_id': "ID of the local cross-connect", + # 'pp_info': "Patch panel ID and port number(s)" } def __init__(self, *args, **kwargs): super(CircuitForm, self).__init__(*args, **kwargs) + # # If this circuit has been assigned to an interface, initialize rack and device + # if self.instance.interface: + # self.initial['rack'] = self.instance.interface.device.rack + # self.initial['device'] = self.instance.interface.device + # + # # Limit rack choices + # if self.is_bound: + # self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site']) + # elif self.initial.get('site'): + # self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site']) + # else: + # self.fields['rack'].choices = [] + # + # # Limit device choices + # if self.is_bound and self.data.get('rack'): + # self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack']) + # elif self.initial.get('rack'): + # self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack']) + # else: + # self.fields['device'].choices = [] + # + # # Limit interface choices + # if self.is_bound and self.data.get('device'): + # interfaces = Interface.objects.filter(device=self.data['device'])\ + # .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + # self.fields['interface'].widget.attrs['initial'] = self.data.get('interface') + # elif self.initial.get('device'): + # interfaces = Interface.objects.filter(device=self.initial['device'])\ + # .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + # self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface') + # else: + # interfaces = [] + # self.fields['interface'].choices = [ + # (iface.id, { + # 'label': iface.name, + # 'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'), + # }) for iface in interfaces + # ] + + +class CircuitFromCSVForm(forms.ModelForm): + provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name', + error_messages={'invalid_choice': 'Provider not found.'}) + type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name', + error_messages={'invalid_choice': 'Invalid circuit type.'}) + tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, + error_messages={'invalid_choice': 'Tenant not found.'}) + # site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name', + # error_messages={'invalid_choice': 'Site not found.'}) + + class Meta: + model = Circuit + fields = ['cid', 'provider', 'type', 'tenant', 'install_date'] + # fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed', + # 'commit_rate', 'xconnect_id', 'pp_info'] + + +class CircuitImportForm(BulkImportForm, BootstrapMixin): + csv = CSVDataField(csv_form=CircuitFromCSVForm) + + +class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput) + type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False) + provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + # port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)') + # commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') + comments = CommentField() + + +def circuit_type_choices(): + type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits')) + return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices] + + +def circuit_provider_choices(): + provider_choices = Provider.objects.annotate(circuit_count=Count('circuits')) + return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices] + + +def circuit_tenant_choices(): + tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits')) + return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices] + + +# def circuit_site_choices(): +# site_choices = Site.objects.annotate(circuit_count=Count('circuits')) +# return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices] + + +class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = Circuit + type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices) + provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices, + widget=forms.SelectMultiple(attrs={'size': 8})) + tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices, + widget=forms.SelectMultiple(attrs={'size': 8})) + # site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices, + # widget=forms.SelectMultiple(attrs={'size': 8})) + +# +# Terminations +# +class TerminationForm(BootstrapMixin, CustomFieldForm): + site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) + rack = forms.ModelChoiceField( + queryset=Rack.objects.all(), + required=False, + label='Rack', + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', + attrs={'filter-for': 'device'} + ) + ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + label='Device', + widget=APISelect( + api_url='/api/dcim/devices/?rack_id={{rack}}', + attrs={'filter-for': 'interface'} + ) + ) + livesearch = forms.CharField( + required=False, + label='Device', + widget=Livesearch( + query_key='q', + query_url='dcim-api:device_list', + field_to_update='device' + ) + ) + interface = forms.ModelChoiceField( + queryset=Interface.objects.all(), + required=False, + label='Interface', + widget=APISelect( + api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical', + disabled_indicator='is_connected' + ) + ) + comments = CommentField() + + class Meta: + model = Termination + fields = [ + 'tid', 'site', 'rack', 'device', 'livesearch', + 'interface','port_speed', 'upstream_speed', 'commit_rate', + 'xconnect_id', 'pp_info', 'comments' + ] + help_texts = { + 'tid': "Termination ID", + 'port_speed': "Physical circuit speed", + 'commit_rate': "Commited rate", + 'xconnect_id': "ID of the local cross-connect", + 'pp_info': "Patch panel ID and port number(s)" + } + widgets = { + 'circuit': forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + + super(TerminationForm, self).__init__(*args, **kwargs) + # If this circuit has been assigned to an interface, initialize rack and device if self.instance.interface: self.initial['rack'] = self.instance.interface.device.rack @@ -143,11 +312,11 @@ class CircuitForm(BootstrapMixin, CustomFieldForm): # Limit interface choices if self.is_bound and self.data.get('device'): interfaces = Interface.objects.filter(device=self.data['device'])\ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') self.fields['interface'].widget.attrs['initial'] = self.data.get('interface') elif self.initial.get('device'): interfaces = Interface.objects.filter(device=self.initial['device'])\ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface') else: interfaces = [] @@ -157,64 +326,3 @@ class CircuitForm(BootstrapMixin, CustomFieldForm): 'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'), }) for iface in interfaces ] - - -class CircuitFromCSVForm(forms.ModelForm): - provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name', - error_messages={'invalid_choice': 'Provider not found.'}) - type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name', - error_messages={'invalid_choice': 'Invalid circuit type.'}) - tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, - error_messages={'invalid_choice': 'Tenant not found.'}) - site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name', - error_messages={'invalid_choice': 'Site not found.'}) - - class Meta: - model = Circuit - fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed', - 'commit_rate', 'xconnect_id', 'pp_info'] - - -class CircuitImportForm(BulkImportForm, BootstrapMixin): - csv = CSVDataField(csv_form=CircuitFromCSVForm) - - -class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): - pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput) - type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False) - provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') - port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)') - commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') - comments = CommentField() - - -def circuit_type_choices(): - type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits')) - return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices] - - -def circuit_provider_choices(): - provider_choices = Provider.objects.annotate(circuit_count=Count('circuits')) - return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices] - - -def circuit_tenant_choices(): - tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits')) - return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices] - - -def circuit_site_choices(): - site_choices = Site.objects.annotate(circuit_count=Count('circuits')) - return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices] - - -class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): - model = Circuit - type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices) - provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices, - widget=forms.SelectMultiple(attrs={'size': 8})) - tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices, - widget=forms.SelectMultiple(attrs={'size': 8})) - site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices, - widget=forms.SelectMultiple(attrs={'size': 8})) 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..eb5e6af22 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,42 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): def to_csv(self): return ','.join([ + self.cid, + self.provider.name, + self.type.name, + self.tenant.name if self.tenant else '', + self.install_date.isoformat() if self.install_date else '', + ]) + +class Termination(CreatedUpdatedModel): + """ + A Termination is where a site + """ + circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.PROTECT) + tid = models.CharField(max_length=50, verbose_name='Termination ID') + site = models.ForeignKey(Site, related_name='terminations', on_delete=models.PROTECT) + interface = models.OneToOneField(Interface, related_name='termination', blank=True, null=True) + port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)') + upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)', + help_text='Upstream speed, if different from port speed') + commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)') + xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') + pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') + comments = models.TextField(blank=True) + + class Meta: + ordering = ['circuit', 'tid'] + unique_together = ['circuit', 'tid'] + + def __unicode__(self): + return u'{} Termination {}'.format(self.circuit.cid, self.tid) + + def get_absolute_url(self): + return reverse('circuits:circuit', args=[self.circuit.pk]) + + def to_csv(self): + return ','.join([ + self.tid, self.cid, self.provider.name, self.type.name, diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index f82459890..59f0f59b4 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -56,12 +56,10 @@ class CircuitTable(BaseTable): type = tables.Column(verbose_name='Type') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'), - verbose_name='Port Speed') - commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'), - verbose_name='Commit Rate') + termination_count = tables.Column(accessor=Accessor('count_terminations'), + verbose_name='Terminations') + class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate') + fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_count') 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..b56acbc6b 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,61 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuit' cls = Circuit default_redirect_url = 'circuits:circuit_list' + +# +# Terminations +# +@permission_required('circuits.change_circuit') +def termination_add(request, pk): + + circuit = get_object_or_404(Circuit, pk=pk) + + if request.method == 'POST': + form = forms.TerminationForm(request.POST) + if form.is_valid(): + if not form.errors: + new_termination = form.save(commit=False) + new_termination.circuit = circuit + new_termination.save() + if '_addanother' in request.POST: + return redirect('circuits:termination_add', pk=circuit.pk) + else: + return redirect('circuits:circuit', pk=circuit.pk) + + else: + form = forms.TerminationForm() + + return render(request, 'circuits/termination_edit.html', { + 'form': form, + 'obj_type': "Termination", + 'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}), + }) + +class TerminationEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.change_circuit' + model = Termination + form_class = forms.TerminationForm + fields_initial = ['site'] + template_name = 'circuits/termination_edit.html' + cancel_url = 'circuits:circuit_list' + +@permission_required('circuits.delete_circuit') +def termination_delete(request, pk): + + termination = get_object_or_404(Termination, pk=pk) + + if request.method == 'POST': + form = ConfirmationForm(request.POST) + if form.is_valid(): + termination.delete() + messages.success(request, "Termination {0} has been deleted from {1}".format(termination, termination.circuit)) + return redirect('circuits:circuit', pk=termination.circuit.pk) + + else: + form = ConfirmationForm() + + return render(request, 'circuits/termination_delete.html', { + 'termination': termination, + 'form': form, + 'cancel_url': reverse('circuits:circuit', kwargs={'pk': termination.circuit.pk}), + }) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 8d2e48430..21671234c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1121,7 +1121,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin): # Initialize interface A choices device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \ - .select_related('circuit', 'connected_as_a', 'connected_as_b') + .select_related('termination', 'connected_as_a', 'connected_as_b') self.fields['interface_a'].choices = [ (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces ] @@ -1137,10 +1137,10 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin): # Initialize interface_b choices if device_b is set if self.is_bound: device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') elif self.initial.get('device_b'): device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') + .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') else: device_b_interfaces = [] self.fields['interface_b'].choices = [ diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b8185881f..f46cb2655 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -269,7 +269,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): @property def count_circuits(self): - return self.circuits.count() + return self.terminations.count() # @@ -1052,7 +1052,7 @@ class Interface(models.Model): @property def is_connected(self): try: - return bool(self.circuit) + return bool(self.termination) except ObjectDoesNotExist: pass return bool(self.connection) 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/netbox/urls.py b/netbox/netbox/urls.py index b67f04cfd..4b17bbb91 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -42,3 +42,4 @@ urlpatterns = [ url(r'^admin/', include(admin.site.urls)), ] + 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

From 2ccaae42a818851ed4274da644fe202ea236492e Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 13:03:16 -0500 Subject: [PATCH 2/9] Undo change to .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index de6c25552..954607b60 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,3 @@ configuration.py !upgrade.sh fabfile.py *.swp -*.bak -gunicorn_config.py -netbox/static From 08bc5df9e30fcd065a6517ce46e5d3ac6cbbdb52 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 13:22:55 -0500 Subject: [PATCH 3/9] Fixed provider filter by site name --- netbox/circuits/filters.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 0d76308c6..b9f37acad 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -13,17 +13,17 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search', label='Search', ) - # site_id = django_filters.ModelMultipleChoiceFilter( - # name='circuits__site', - # queryset=Site.objects.all(), - # label='Site', - # ) - # site = django_filters.ModelMultipleChoiceFilter( - # name='circuits__site', - # queryset=Site.objects.all(), - # to_field_name='slug', - # label='Site (slug)', - # ) + site_id = django_filters.ModelMultipleChoiceFilter( + name='circuits__terminations__site', + queryset=Site.objects.all(), + label='Site', + ) + site = django_filters.ModelMultipleChoiceFilter( + name='circuits__terminations__site', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site (slug)', + ) class Meta: model = Provider From 5b78bb0738eae70d9859519ca8c41f015550b7d5 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 13:33:37 -0500 Subject: [PATCH 4/9] Fixed Multipoint filtering --- netbox/circuits/forms.py | 73 ---------------------------------------- 1 file changed, 73 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 9c389fe41..677daa082 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -85,19 +85,6 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin): # class CircuitForm(BootstrapMixin, CustomFieldForm): - # site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) - # rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack', - # widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', - # attrs={'filter-for': 'device'})) - # device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', - # widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', - # attrs={'filter-for': 'interface'})) - # livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( - # query_key='q', query_url='dcim-api:device_list', field_to_update='device') - # ) - # interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface', - # widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical', - # disabled_indicator='is_connected')) comments = CommentField() class Meta: @@ -105,62 +92,15 @@ class CircuitForm(BootstrapMixin, CustomFieldForm): fields = [ 'cid', 'type', 'provider', 'tenant', 'install_date', 'comments' ] - # fields = [ - # 'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date', - # 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments' - # ] help_texts = { 'cid': "Unique circuit ID", 'install_date': "Format: YYYY-MM-DD", - # 'port_speed': "Physical circuit speed", - # 'commit_rate': "Commited rate", - # 'xconnect_id': "ID of the local cross-connect", - # 'pp_info': "Patch panel ID and port number(s)" } def __init__(self, *args, **kwargs): super(CircuitForm, self).__init__(*args, **kwargs) - # # If this circuit has been assigned to an interface, initialize rack and device - # if self.instance.interface: - # self.initial['rack'] = self.instance.interface.device.rack - # self.initial['device'] = self.instance.interface.device - # - # # Limit rack choices - # if self.is_bound: - # self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site']) - # elif self.initial.get('site'): - # self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site']) - # else: - # self.fields['rack'].choices = [] - # - # # Limit device choices - # if self.is_bound and self.data.get('rack'): - # self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack']) - # elif self.initial.get('rack'): - # self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack']) - # else: - # self.fields['device'].choices = [] - # - # # Limit interface choices - # if self.is_bound and self.data.get('device'): - # interfaces = Interface.objects.filter(device=self.data['device'])\ - # .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') - # self.fields['interface'].widget.attrs['initial'] = self.data.get('interface') - # elif self.initial.get('device'): - # interfaces = Interface.objects.filter(device=self.initial['device'])\ - # .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') - # self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface') - # else: - # interfaces = [] - # self.fields['interface'].choices = [ - # (iface.id, { - # 'label': iface.name, - # 'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'), - # }) for iface in interfaces - # ] - class CircuitFromCSVForm(forms.ModelForm): provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name', @@ -169,14 +109,10 @@ class CircuitFromCSVForm(forms.ModelForm): error_messages={'invalid_choice': 'Invalid circuit type.'}) tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, error_messages={'invalid_choice': 'Tenant not found.'}) - # site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name', - # error_messages={'invalid_choice': 'Site not found.'}) class Meta: model = Circuit fields = ['cid', 'provider', 'type', 'tenant', 'install_date'] - # fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed', - # 'commit_rate', 'xconnect_id', 'pp_info'] class CircuitImportForm(BulkImportForm, BootstrapMixin): @@ -188,8 +124,6 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False) provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') - # port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)') - # commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') comments = CommentField() @@ -208,11 +142,6 @@ def circuit_tenant_choices(): return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices] -# def circuit_site_choices(): -# site_choices = Site.objects.annotate(circuit_count=Count('circuits')) -# return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices] - - class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Circuit type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices) @@ -220,8 +149,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): widget=forms.SelectMultiple(attrs={'size': 8})) tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices, widget=forms.SelectMultiple(attrs={'size': 8})) - # site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices, - # widget=forms.SelectMultiple(attrs={'size': 8})) # # Terminations From 510bf5ca67700068b6b6b1c03ba6efa1b21d6968 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 16:06:23 -0500 Subject: [PATCH 5/9] Fixed PEP8 issues with multipoint circuits --- netbox/circuits/admin.py | 3 ++- netbox/circuits/forms.py | 3 ++- netbox/circuits/models.py | 1 + netbox/circuits/tables.py | 3 +-- netbox/circuits/views.py | 3 +++ netbox/netbox/urls.py | 1 - 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/netbox/circuits/admin.py b/netbox/circuits/admin.py index 8a194a6e6..3e730682f 100644 --- a/netbox/circuits/admin.py +++ b/netbox/circuits/admin.py @@ -21,13 +21,14 @@ class CircuitTypeAdmin(admin.ModelAdmin): @admin.register(Circuit) class CircuitAdmin(admin.ModelAdmin): - list_display = ['cid', 'provider', 'type', 'tenant','install_date'] + list_display = ['cid', 'provider', 'type', 'tenant', 'install_date'] list_filter = ['provider', 'type', 'tenant'] def get_queryset(self, request): qs = super(CircuitAdmin, self).get_queryset(request) return qs.select_related('provider', 'type', 'tenant') + @admin.register(Termination) class TerminationAdmin(admin.ModelAdmin): list_display = ['tid', 'circuit', 'site', 'port_speed_human', diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 677daa082..6db3076a4 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -150,6 +150,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices, widget=forms.SelectMultiple(attrs={'size': 8})) + # # Terminations # @@ -197,7 +198,7 @@ class TerminationForm(BootstrapMixin, CustomFieldForm): model = Termination fields = [ 'tid', 'site', 'rack', 'device', 'livesearch', - 'interface','port_speed', 'upstream_speed', 'commit_rate', + 'interface', 'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments' ] help_texts = { diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index eb5e6af22..603e3b9e9 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -90,6 +90,7 @@ class Circuit(CreatedUpdatedModel): self.install_date.isoformat() if self.install_date else '', ]) + class Termination(CreatedUpdatedModel): """ A Termination is where a site diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 59f0f59b4..761583e16 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -57,8 +57,7 @@ class CircuitTable(BaseTable): provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') termination_count = tables.Column(accessor=Accessor('count_terminations'), - verbose_name='Terminations') - + verbose_name='Terminations') class Meta(BaseTable.Meta): model = Circuit diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index b56acbc6b..08a468821 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -160,6 +160,7 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): cls = Circuit default_redirect_url = 'circuits:circuit_list' + # # Terminations # @@ -189,6 +190,7 @@ def termination_add(request, pk): 'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}), }) + class TerminationEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'circuits.change_circuit' model = Termination @@ -197,6 +199,7 @@ class TerminationEditView(PermissionRequiredMixin, ObjectEditView): template_name = 'circuits/termination_edit.html' cancel_url = 'circuits:circuit_list' + @permission_required('circuits.delete_circuit') def termination_delete(request, pk): diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 4b17bbb91..b67f04cfd 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -42,4 +42,3 @@ urlpatterns = [ url(r'^admin/', include(admin.site.urls)), ] - From 3d59b406aeca16948ba59c07f49e84a93aa77667 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 16:32:49 -0500 Subject: [PATCH 6/9] Re-added terminations to forms.py --- netbox/circuits/forms.py | 106 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 55f4a0dbf..1742d8828 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -192,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 + ] From 047ccaa14ea656dc0b12facb47fefd4c4713a644 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 17:58:34 -0500 Subject: [PATCH 7/9] Fixed pep8 error --- netbox/utilities/forms.py | 1 - 1 file changed, 1 deletion(-) 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 # From 6510c6e70e861f70c66f162a57a5433e68378896 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 18:29:16 -0500 Subject: [PATCH 8/9] Fixed issues with terminations in dcim and APIs --- netbox/circuits/api/serializers.py | 5 +---- netbox/circuits/api/views.py | 4 ++-- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/api/views.py | 2 +- netbox/dcim/tests/test_apis.py | 1 - 5 files changed, 5 insertions(+), 9 deletions(-) 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/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/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 = [ From ffe374c54ca2db9b54c66f5f18d6832a7299a324 Mon Sep 17 00:00:00 2001 From: Nathan Gotz Date: Wed, 14 Sep 2016 20:57:01 -0500 Subject: [PATCH 9/9] Modified gitignore to not include gunicorn --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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/ +