mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-19 03:42:25 -06:00
Initial push to public repo
This commit is contained in:
0
netbox/circuits/__init__.py
Normal file
0
netbox/circuits/__init__.py
Normal file
30
netbox/circuits/admin.py
Normal file
30
netbox/circuits/admin.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Provider, CircuitType, Circuit
|
||||
|
||||
|
||||
@admin.register(Provider)
|
||||
class ProviderAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'asn']
|
||||
|
||||
|
||||
@admin.register(CircuitType)
|
||||
class CircuitTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
@admin.register(Circuit)
|
||||
class CircuitAdmin(admin.ModelAdmin):
|
||||
list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
|
||||
list_filter = ['provider']
|
||||
exclude = ['interface']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(CircuitAdmin, self).get_queryset(request)
|
||||
return qs.select_related('provider', 'type', 'site')
|
||||
0
netbox/circuits/api/__init__.py
Normal file
0
netbox/circuits/api/__init__.py
Normal file
60
netbox/circuits/api/serializers.py
Normal file
60
netbox/circuits/api/serializers.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
|
||||
class ProviderNestedSerializer(ProviderSerializer):
|
||||
|
||||
class Meta(ProviderSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
||||
|
||||
class Meta(CircuitTypeSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitSerializer(serializers.ModelSerializer):
|
||||
provider = ProviderNestedSerializer()
|
||||
type = CircuitTypeNestedSerializer()
|
||||
site = SiteNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
|
||||
'xconnect_id', 'comments']
|
||||
|
||||
|
||||
class CircuitNestedSerializer(CircuitSerializer):
|
||||
|
||||
class Meta(CircuitSerializer.Meta):
|
||||
fields = ['id', 'cid']
|
||||
24
netbox/circuits/api/urls.py
Normal file
24
netbox/circuits/api/urls.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.models import GRAPH_TYPE_PROVIDER
|
||||
from extras.api.views import GraphListView
|
||||
|
||||
from .views import *
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
url(r'^providers/$', ProviderListView.as_view(), name='provider_list'),
|
||||
url(r'^providers/(?P<pk>\d+)/$', ProviderDetailView.as_view(), name='provider_detail'),
|
||||
url(r'^providers/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER}, name='provider_graphs'),
|
||||
|
||||
# Circuit types
|
||||
url(r'^circuit-types/$', CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
url(r'^circuit-types/(?P<pk>\d+)/$', CircuitTypeDetailView.as_view(), name='circuittype_detail'),
|
||||
|
||||
# Circuits
|
||||
url(r'^circuits/$', CircuitListView.as_view(), name='circuit_list'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', CircuitDetailView.as_view(), name='circuit_detail'),
|
||||
|
||||
]
|
||||
54
netbox/circuits/api/views.py
Normal file
54
netbox/circuits/api/views.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from rest_framework import generics
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from circuits.filters import CircuitFilter
|
||||
from .serializers import ProviderSerializer, CircuitTypeSerializer, CircuitSerializer
|
||||
|
||||
|
||||
class ProviderListView(generics.ListAPIView):
|
||||
"""
|
||||
List all providers
|
||||
"""
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
|
||||
|
||||
class ProviderDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single provider
|
||||
"""
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
|
||||
|
||||
class CircuitTypeListView(generics.ListAPIView):
|
||||
"""
|
||||
List all circuit types
|
||||
"""
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = CircuitTypeSerializer
|
||||
|
||||
|
||||
class CircuitTypeDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit type
|
||||
"""
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = CircuitTypeSerializer
|
||||
|
||||
|
||||
class CircuitListView(generics.ListAPIView):
|
||||
"""
|
||||
List circuits (filterable)
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
||||
serializer_class = CircuitSerializer
|
||||
filter_class = CircuitFilter
|
||||
|
||||
|
||||
class CircuitDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
||||
serializer_class = CircuitSerializer
|
||||
52
netbox/circuits/filters.py
Normal file
52
netbox/circuits/filters.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import django_filters
|
||||
|
||||
from dcim.models import Site
|
||||
from circuits.models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
class CircuitFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type',
|
||||
queryset=CircuitType.objects.all(),
|
||||
label='Circuit type (ID)',
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type',
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Circuit type (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']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(cid__icontains=value)
|
||||
191
netbox/circuits/forms.py
Normal file
191
netbox/circuits/forms.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from utilities.forms import BootstrapMixin, SmallTextarea, ConfirmationForm, APISelect, Livesearch
|
||||
|
||||
from .models import PORT_SPEED_CHOICES, Circuit, Provider, CircuitType
|
||||
from utilities.forms import CommentField, CSVDataField, BulkImportForm
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderForm(forms.ModelForm, BootstrapMixin):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
||||
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the provider",
|
||||
'slug': "URL-friendly unique shorthand (e.g. 'decix' for DE-CIX)",
|
||||
'asn': "BGP autonomous system number (if applicable)",
|
||||
'portal_url': "URL of the provider's customer support portal",
|
||||
'noc_contact': "NOC email address and phone number",
|
||||
'admin_contact': "Administrative contact email address and phone number",
|
||||
}
|
||||
|
||||
|
||||
class ProviderFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
|
||||
class ProviderImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
asn = forms.IntegerField(required=False, label='ASN')
|
||||
account = forms.CharField(max_length=30, required=False, label='Account number')
|
||||
portal_url = forms.URLField(required=False, label='Portal')
|
||||
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
|
||||
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class ProviderBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitForm(forms.ModelForm, BootstrapMixin):
|
||||
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(), 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', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
||||
'port_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 (in Mbps)",
|
||||
'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.'})
|
||||
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', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
|
||||
'pp_info']
|
||||
|
||||
|
||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
|
||||
|
||||
class CircuitBulkEditForm(forms.Form, BootstrapMixin):
|
||||
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)
|
||||
port_speed = forms.ChoiceField(choices=[(None, '---------')] + PORT_SPEED_CHOICES, required=False,
|
||||
label='Port speed')
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Mbps)')
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class CircuitBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def circuit_type_choices():
|
||||
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(t.slug, '{} ({})'.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, '{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
||||
|
||||
|
||||
def circuit_site_choices():
|
||||
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
||||
|
||||
|
||||
class CircuitFilterForm(forms.Form, BootstrapMixin):
|
||||
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
|
||||
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
82
netbox/circuits/migrations/0001_initial.py
Normal file
82
netbox/circuits/migrations/0001_initial.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('dcim', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Circuit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')),
|
||||
('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')),
|
||||
('port_speed', models.PositiveSmallIntegerField(choices=[[100, b'100 Mbps'], [1000, b'1 Gbps'], [10000, b'10 Gbps'], [25000, b'25 Gbps'], [40000, b'40 Gbps'], [50000, b'50 Gbps'], [100000, b'100 Gbps']], verbose_name=b'Port speed')),
|
||||
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Mbps)')),
|
||||
('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)),
|
||||
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['provider', 'cid'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitType',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Provider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('asn', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'ASN')),
|
||||
('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')),
|
||||
('portal_url', models.URLField(blank=True, verbose_name=b'Portal')),
|
||||
('noc_contact', models.TextField(blank=True, verbose_name=b'NOC Contact')),
|
||||
('admin_contact', models.TextField(blank=True, verbose_name=b'Admin Contact')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuit',
|
||||
unique_together=set([('provider', 'cid')]),
|
||||
),
|
||||
]
|
||||
0
netbox/circuits/migrations/__init__.py
Normal file
0
netbox/circuits/migrations/__init__.py
Normal file
86
netbox/circuits/models.py
Normal file
86
netbox/circuits/models.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
|
||||
from dcim.models import Site, Interface
|
||||
|
||||
|
||||
PORT_SPEED_100M = 100
|
||||
PORT_SPEED_1G = 1000
|
||||
PORT_SPEED_10G = 10000
|
||||
PORT_SPEED_25G = 25000
|
||||
PORT_SPEED_40G = 40000
|
||||
PORT_SPEED_50G = 50000
|
||||
PORT_SPEED_100G = 100000
|
||||
PORT_SPEED_CHOICES = [
|
||||
[PORT_SPEED_100M, '100 Mbps'],
|
||||
[PORT_SPEED_1G, '1 Gbps'],
|
||||
[PORT_SPEED_10G, '10 Gbps'],
|
||||
[PORT_SPEED_25G, '25 Gbps'],
|
||||
[PORT_SPEED_40G, '40 Gbps'],
|
||||
[PORT_SPEED_50G, '50 Gbps'],
|
||||
[PORT_SPEED_100G, '100 Gbps'],
|
||||
]
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
"""
|
||||
A transit provider, IX, or direct peer
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
||||
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
||||
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.slug])
|
||||
|
||||
|
||||
class CircuitType(models.Model):
|
||||
"""
|
||||
A type of circuit
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Circuit(models.Model):
|
||||
"""
|
||||
A data circuit from a site to a provider (includes IX connections)
|
||||
"""
|
||||
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
||||
type = models.ForeignKey('CircuitType', related_name='circuits', 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.PositiveSmallIntegerField(choices=PORT_SPEED_CHOICES, verbose_name='Port speed')
|
||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Mbps)')
|
||||
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 = ['provider', 'cid']
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __unicode__(self):
|
||||
return "{0} {1}".format(self.provider, self.cid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
59
netbox/circuits/tables.py
Normal file
59
netbox/circuits/tables.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from .models import Circuit, Provider
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderTable(tables.Table):
|
||||
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ('name', 'asn', 'circuit_count')
|
||||
empty_text = "No providers found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class ProviderBulkEditTable(ProviderTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(ProviderTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'asn', 'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTable(tables.Table):
|
||||
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
|
||||
type = tables.Column(verbose_name='Type')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
port_speed = tables.Column(verbose_name='Port Speed')
|
||||
commit_rate = tables.Column(verbose_name='Commit (Mbps)')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ('cid', 'type', 'provider', 'site', 'port_speed', 'commit_rate')
|
||||
empty_text = "No circuits found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class CircuitBulkEditTable(CircuitTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(CircuitTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed', 'commit_rate')
|
||||
3
netbox/circuits/tests.py
Normal file
3
netbox/circuits/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
23
netbox/circuits/urls.py
Normal file
23
netbox/circuits/urls.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^circuits/$', views.circuit_list, name='circuit_list'),
|
||||
url(r'^circuits/add/$', views.circuit_add, name='circuit_add'),
|
||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.circuit_edit, name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.circuit_delete, name='circuit_delete'),
|
||||
|
||||
url(r'^providers/$', views.provider_list, name='provider_list'),
|
||||
url(r'^providers/add/$', views.provider_add, name='provider_add'),
|
||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.provider_edit, name='provider_edit'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.provider_delete, name='provider_delete'),
|
||||
]
|
||||
309
netbox/circuits/views.py
Normal file
309
netbox/circuits/views.py
Normal file
@@ -0,0 +1,309 @@
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, ProtectedError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import BulkImportView, BulkEditView, BulkDeleteView
|
||||
|
||||
from .filters import CircuitFilter
|
||||
from .forms import CircuitForm, CircuitImportForm, CircuitBulkEditForm, CircuitBulkDeleteForm, CircuitFilterForm, \
|
||||
ProviderForm, ProviderImportForm, ProviderBulkEditForm, ProviderBulkDeleteForm
|
||||
from .models import Circuit, Provider
|
||||
from .tables import CircuitTable, CircuitBulkEditTable, ProviderTable, ProviderBulkEditTable
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
def provider_list(request):
|
||||
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='provider', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_providers')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('circuits.change_provider') or request.user.has_perm('circuits.delete_provider'):
|
||||
provider_table = ProviderBulkEditTable(queryset)
|
||||
else:
|
||||
provider_table = ProviderTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(provider_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='provider')
|
||||
|
||||
return render(request, 'circuits/provider_list.html', {
|
||||
'provider_table': provider_table,
|
||||
'export_templates': export_templates,
|
||||
})
|
||||
|
||||
|
||||
def provider(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.add_provider')
|
||||
def provider_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ProviderForm(request.POST)
|
||||
if form.is_valid():
|
||||
provider = form.save()
|
||||
messages.success(request, "Added new provider: {0}".format(provider))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('circuits:provider_add')
|
||||
else:
|
||||
return redirect('circuits:provider', slug=provider.slug)
|
||||
|
||||
else:
|
||||
form = ProviderForm()
|
||||
|
||||
return render(request, 'circuits/provider_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:provider_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.change_provider')
|
||||
def provider_edit(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ProviderForm(request.POST, instance=provider)
|
||||
if form.is_valid():
|
||||
provider = form.save()
|
||||
messages.success(request, "Modified provider {0}".format(provider))
|
||||
return redirect('circuits:provider', slug=provider.slug)
|
||||
|
||||
else:
|
||||
form = ProviderForm(instance=provider)
|
||||
|
||||
return render(request, 'circuits/provider_edit.html', {
|
||||
'provider': provider,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:provider', kwargs={'slug': provider.slug}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.delete_provider')
|
||||
def provider_delete(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
provider.delete()
|
||||
messages.success(request, "Provider {0} has been deleted".format(provider))
|
||||
return redirect('circuits:provider_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(provider, request, e)
|
||||
return redirect('circuits:provider', slug=provider.slug)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'circuits/provider_delete.html', {
|
||||
'provider': provider,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:provider', kwargs={'slug': provider.slug})
|
||||
})
|
||||
|
||||
|
||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
form = ProviderImportForm
|
||||
table = ProviderTable
|
||||
template_name = 'circuits/provider_import.html'
|
||||
obj_list_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
cls = Provider
|
||||
form = ProviderBulkEditForm
|
||||
template_name = 'circuits/provider_bulk_edit.html'
|
||||
redirect_url = 'circuits:provider_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} providers".format(updated_count))
|
||||
|
||||
|
||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
cls = Provider
|
||||
form = ProviderBulkDeleteForm
|
||||
template_name = 'circuits/provider_bulk_delete.html'
|
||||
redirect_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
def circuit_list(request):
|
||||
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'site')
|
||||
queryset = CircuitFilter(request.GET, queryset).qs
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='circuit', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_circuits')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('circuits.change_circuit') or request.user.has_perm('circuits.delete_circuit'):
|
||||
circuit_table = CircuitBulkEditTable(queryset)
|
||||
else:
|
||||
circuit_table = CircuitTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(circuit_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='circuit')
|
||||
|
||||
return render(request, 'circuits/circuit_list.html', {
|
||||
'circuit_table': circuit_table,
|
||||
'export_templates': export_templates,
|
||||
'filter_form': CircuitFilterForm(request.GET, label_suffix=''),
|
||||
})
|
||||
|
||||
|
||||
def circuit(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.add_circuit')
|
||||
def circuit_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CircuitForm(request.POST)
|
||||
if form.is_valid():
|
||||
circuit = form.save()
|
||||
messages.success(request, "Added new circuit: {0}".format(circuit))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('circuits:circuit_add')
|
||||
else:
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = CircuitForm(initial={
|
||||
'site': request.GET.get('site'),
|
||||
})
|
||||
|
||||
return render(request, 'circuits/circuit_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:circuit_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.change_circuit')
|
||||
def circuit_edit(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CircuitForm(request.POST, instance=circuit)
|
||||
if form.is_valid():
|
||||
circuit = form.save()
|
||||
messages.success(request, "Modified circuit {0}".format(circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = CircuitForm(instance=circuit)
|
||||
|
||||
return render(request, 'circuits/circuit_edit.html', {
|
||||
'circuit': circuit,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.delete_circuit')
|
||||
def circuit_delete(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
circuit.delete()
|
||||
messages.success(request, "Circuit {0} has been deleted".format(circuit))
|
||||
return redirect('circuits:circuit_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(circuit, request, e)
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'circuits/circuit_delete.html', {
|
||||
'circuit': circuit,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk})
|
||||
})
|
||||
|
||||
|
||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
form = CircuitImportForm
|
||||
table = CircuitTable
|
||||
template_name = 'circuits/circuit_import.html'
|
||||
obj_list_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
cls = Circuit
|
||||
form = CircuitBulkEditForm
|
||||
template_name = 'circuits/circuit_bulk_edit.html'
|
||||
redirect_url = 'circuits:circuit_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} circuits".format(updated_count))
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
form = CircuitBulkDeleteForm
|
||||
template_name = 'circuits/circuit_bulk_delete.html'
|
||||
redirect_url = 'circuits:circuit_list'
|
||||
Reference in New Issue
Block a user