Merge remote-tracking branch 'rdujardin/develop' into develop

Note : Adding DNS conflicted with adding Tenant fields and modifying VRF fields in IPAM.
This commit is contained in:
rdujardin 2016-08-03 11:00:42 +02:00
commit 7dafbae4bd
57 changed files with 2336 additions and 27 deletions

41
docs/data-model/dns.md Normal file
View File

@ -0,0 +1,41 @@
The DNS component of NetBox deals with the management of DNS zones.
# Zones
A zone corresponds to a zone file in a DNS server, it stores the SOA (Start Of Authority) record and other records that are stored as Record objects.
Zone objects handle only forward DNS, reverse DNS is handled by Prefixes (in IPAM section), which also store a SOA record.
Netbox provides two views in the DNS menu to get the exports in BIND format, which is compatible with every DNS server, directly or by import. Those
exports are also accessible as JSON through the REST API. One of these views is the export of all the forward zones in the database,
the second is the export of all the reverse zones.
The reverse zones are correctly merged and/or divided to meet the requirements of a DNS server (for instance, IPv4 reverse zones must be /16 or /24), and
not to duplicate records (for instance if you have in database the prefixes 192.168.0.0/16 and 192.168.1.0/24, only the biggest will be exported) ; however,
only IP addresses which are in an active prefix will be taken into account. Obviously, reverse DNS is supported for both IPv4 and IPv6.
The SOA Serial field is not editable : it's automatically created and managed by Netbox. Each time a zone (forward or reverse) is exported,
if there are changes since the last export or if it's the first export, the serial will be incremented. It's in the following format :
YYYYMMDDNN with Y the year, M the month, D the day and N a two-digit counter.
As zones and their BIND exports are readable through the REST API, it is possible to write some external script to automatically update
your DNS server configuration from Netbox's database.
---
# Record
Each Record object represents a DNS record, i.e. a link between a hostname and a resource, which can be either an IP address or a text value,
for instance another name if the record is of CNAME type.
Records must be linked to an existing zone, and hold either an IP address link or a text value. The "Address" field points to an IP address
in database, but if you want to put an IP in your record but not in your database (if you don't own the IP for instance), it's possible
by putting the IP as text value instead.
You can create, edit or import records with IPs not existing yet in the database. They will be automatically created (but not the prefixes !).
However, the zones must be created first, they won't be so automatically.
Reverse DNS is not supported by Record objects, but by the "PTR" field in IP addresses. If this field is modified and not empty, a corresponding
A/AAAA record is automatically created if the corresponding zone is found in the database. Be careful, if there was A/AAAA records
for the old PTR value, they are not deleted.

View File

@ -3,6 +3,7 @@
NetBox is an open source web application designed to help manage and document computer networks. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. It encompasses the following aspects of network management:
* **IP address management (IPAM)** - IP networks and addresses, VRFs, and VLANs
* **DNS management** - DNS zones and records
* **Equipment racks** - Organized by group and site
* **Devices** - Types of devices and where they are installed
* **Connections** - Network, console, and power connections among devices

1
netbox/dns/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = 'dns.apps.DNSConfig'

17
netbox/dns/admin.py Normal file
View File

@ -0,0 +1,17 @@
from django.contrib import admin
from .models import (
Zone, Record,
)
@admin.register(Zone)
class ZoneAdmin(admin.ModelAdmin):
list_display = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial']
prepopulated_fields = {
'soa_name': ['name'],
}
@admin.register(Record)
class RecordAdmin(admin.ModelAdmin):
list_display = ['name', 'zone', 'record_type', 'priority', 'address', 'value']

View File

View File

@ -0,0 +1,38 @@
from rest_framework import serializers
from ipam.api.serializers import IPAddressNestedSerializer
from dns.models import Zone, Record
#
# Zones
#
class ZoneSerializer(serializers.ModelSerializer):
class Meta:
model=Zone
fields = ['id', 'name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum', 'description']
class ZoneNestedSerializer(ZoneSerializer):
class Meta(ZoneSerializer.Meta):
fields = ['id', 'name']
#
# Records
#
class RecordSerializer(serializers.ModelSerializer):
zone = ZoneNestedSerializer()
address = IPAddressNestedSerializer()
class Meta:
model=Record
fields = ['id', 'name', 'record_type', 'priority', 'zone', 'address', 'value', 'description']
class RecordNestedSerializer(RecordSerializer):
class Meta(RecordSerializer.Meta):
fields = ['id', 'name', 'record_type', 'zone']

19
netbox/dns/api/urls.py Normal file
View File

@ -0,0 +1,19 @@
from django.conf.urls import url
from .views import *
urlpatterns = [
# Zones
url(r'^zones/$', ZoneListView.as_view(), name='zone_list'),
url(r'^zones/(?P<pk>\d+)/$', ZoneDetailView.as_view(), name='zone_detail'),
# Records
url(r'^records/$', RecordListView.as_view(), name='record_list'),
url(r'^records/(?P<pk>\d+)/$', RecordDetailView.as_view(), name='record_detail'),
# BIND Exports
url(r'^bind/forward/$', bind_forward, name='bind_forward'),
url(r'^bind/reverse/$', bind_reverse, name='bind_reverse'),
]

69
netbox/dns/api/views.py Normal file
View File

@ -0,0 +1,69 @@
from rest_framework import generics
from django.http import HttpResponse
from rest_framework.decorators import api_view
from rest_framework.response import Response
from ipam.models import IPAddress
from dns.models import Zone, Record, export_bind_forward, export_bind_reverse
from dns import filters
from . import serializers
#
# Zones
#
class ZoneListView(generics.ListAPIView):
"""
List all zones
"""
queryset = Zone.objects.all()
serializer_class = serializers.ZoneSerializer
filter_class = filters.ZoneFilter
class ZoneDetailView(generics.RetrieveAPIView):
"""
Retrieve a single zone
"""
queryset = Zone.objects.all()
serializer_class = serializers.ZoneSerializer
#
# Records
#
class RecordListView(generics.ListAPIView):
"""
List all records
"""
queryset = Record.objects.all()
serializer_class = serializers.RecordSerializer
class RecordDetailView(generics.RetrieveAPIView):
"""
Retrieve a single record
"""
queryset = Record.objects.all()
serializer_class = serializers.RecordSerializer
#
# BIND Exports
#
@api_view(['GET'])
def bind_forward(request):
"""
Full export of forward zones in BIND format
"""
zones_list = export_bind_forward()
return Response(zones_list)
@api_view(['GET'])
def bind_reverse(request):
"""
Full export of reverse zones in BIND format
"""
zones_list = export_bind_reverse()
return Response(zones_list)

6
netbox/dns/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DNSConfig(AppConfig):
name = 'dns'
verbose_name='DNS'

51
netbox/dns/filters.py Normal file
View File

@ -0,0 +1,51 @@
import django_filters
from django.db.models import Q
from ipam.models import IPAddress
from .models import (
Zone,
Record,
)
from .forms import record_type_choices
class ZoneFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
name = 'name',
lookup_type = 'icontains',
label = 'Name',
)
class Meta:
model = Zone
fields = ['name']
class RecordFilter(django_filters.FilterSet):
zone__name = django_filters.ModelMultipleChoiceFilter(
name = 'zone__name',
to_field_name = 'name',
lookup_type = 'icontains',
queryset = Zone.objects.all(),
label = 'Zone (Name)',
)
record_type = django_filters.MultipleChoiceFilter(
name = 'record_type',
label = 'Type',
choices = record_type_choices
)
name = django_filters.CharFilter(
name = 'name',
lookup_type = 'icontains',
label = 'Name',
)
name_or_value_or_ip = django_filters.MethodFilter(
name = 'name_or_value_or_ip',
)
class Meta:
model=Record
field = ['name', 'record_type', 'value']
def filter_name_or_value_or_ip(self, queryset, value):
if not value:
return queryset
return queryset.filter(Q(name__icontains=value) | Q(value__icontains=value) | Q(address__address__icontains=value))

View File

@ -0,0 +1,37 @@
[
{
"model": "dns.zone",
"pk": 1,
"fields": {
"name": "foo.net",
"ttl": 10800,
"soa_name": "@",
"soa_contact": "ns@foo.net. noc@foo.net.",
"soa_serial": "2016070401",
"soa_refresh": 3600,
"soa_retry": 3600,
"soa_expire": 604800,
"soa_minimum": 1800
}
},
{
"model": "dns.record",
"pk": 1,
"fields": {
"name": "@",
"record_type": "NS",
"zone": 1,
"value": "ns.foo.net."
}
},
{
"model": "dns.record",
"pk": 2,
"fields": {
"name": "www",
"record_type": "A",
"zone": 1,
"address": 1
}
}
]

41
netbox/dns/formfields.py Normal file
View File

@ -0,0 +1,41 @@
from netaddr import IPNetwork, AddrFormatError
import netaddr
from django import forms
from django.core.exceptions import ValidationError
from ipam.models import IPAddress, Prefix
#
# Form fields
#
class AddressFormField(forms.Field):
default_error_messages = {
'invalid': "Enter a valid IPv4 or IPv6 address (with CIDR mask).",
}
def to_python(self, value):
if not value:
return None
# Ensure that a subnet mask has been specified. This prevents IPs from defaulting to a /32 or /128.
if len(value.split('/')) != 2:
raise ValidationError('CIDR mask (e.g. /24) is required.')
try:
net = IPNetwork(value)
except AddrFormatError:
raise ValidationError("Please specify a valid IPv4 or IPv6 address.")
ip = IPAddress.objects.filter(address=value)
if not ip:
net = IPNetwork(value)
obj = IPAddress(address=net)
obj.save()
return obj
else:
return ip[0]

147
netbox/dns/forms.py Normal file
View File

@ -0,0 +1,147 @@
from django import forms
from django.db.models import Count
from ipam.models import IPAddress
from utilities.forms import (
BootstrapMixin, ConfirmationForm, APISelect, Livesearch, CSVDataField, BulkImportForm,
)
from .models import (
Zone,
Record,
)
from .formfields import AddressFormField
#
# Zones
#
class ZoneForm(forms.ModelForm, BootstrapMixin):
class Meta:
model=Zone
fields = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum', 'description']
labels = {
'soa_name': 'SOA Name',
'soa_contact': 'SOA Contact',
'soa_refresh': 'SOA Refresh',
'soa_retry': 'SOA Retry',
'soa_expire': 'SOA Expire',
'soa_minimum': 'SOA Minimum',
'description': 'Description',
}
help_texts = {
'ttl': "Time to live, in seconds",
'soa_name': "The primary name server for the domain, @ for origin",
'soa_contact': "The responsible party for the domain (e.g. ns.foo.net. noc.foo.net.)",
'soa_refresh': "Refresh time, in seconds",
'soa_retry': "Retry time, in seconds",
'soa_expire': "Expire time, in seconds",
'soa_minimum': "Negative result TTL, in seconds",
}
class ZoneFromCSVForm(forms.ModelForm):
class Meta:
model=Zone
fields = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum', 'description']
class ZoneImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=ZoneFromCSVForm)
class ZoneBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Zone.objects.all(), widget=forms.MultipleHiddenInput)
name = forms.CharField(max_length=100, required=False, label='Name')
ttl = forms.IntegerField(required=False, label='TTL')
soa_name = forms.CharField(max_length=100, required=False, label='SOA Name')
soa_contact = forms.CharField(max_length=100, required=False, label='SOA Contact')
soa_refresh = forms.IntegerField(required=False, label='SOA Refresh')
soa_retry = forms.IntegerField(required=False, label='SOA Retry')
soa_expire = forms.IntegerField(required=False, label='SOA Expire')
soa_minimum = forms.IntegerField(required=False, label='SOA Minimum')
description = forms.CharField(max_length=100, required=False, label='Description')
class ZoneBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Zone.objects.all(), widget=forms.MultipleHiddenInput)
class ZoneFilterForm(forms.Form, BootstrapMixin):
pass
#
# Records
#
class RecordForm(forms.ModelForm, BootstrapMixin):
address = AddressFormField(required=False)
class Meta:
model=Record
fields = ['name', 'record_type', 'priority', 'zone', 'address', 'value', 'description']
labels = {
'record_type': 'Type',
}
help_texts = {
'name': 'Host name, @ for origin (e.g. www)',
'record_type': 'Record type (e.g. MX or AAAA)',
'priority': 'Priority level (e.g. 10)',
'zone': 'Zone the record belongs to',
'address': 'IP address if value is an IP address, in AAAA records for instance',
'value': 'Text value else, in CNAME records for instance'
}
class RecordFromCSVForm(forms.ModelForm):
zone = forms.ModelChoiceField(queryset=Zone.objects.all(), to_field_name='name', error_messages={'invalid_choice': 'Zone not found.'})
address = AddressFormField(required=False)
class Meta:
model=Record
fields = ['zone', 'name', 'record_type', 'priority', 'address', 'value', 'description']
# class RecordBINDImportForm(forms.Form, BootstrapMixin):
# bind = BINDDataField()
# zone_name = CharField(max_length=100, label='Zone')
# slash_v4 = IntegerField()
# def clean(self):
# self.cleaned_data
class RecordImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=RecordFromCSVForm)
class RecordBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Record.objects.all(), widget=forms.MultipleHiddenInput)
name = forms.CharField(max_length=100, required=False, label='Name')
record_type = forms.CharField(max_length=100, required=False, label='Type')
priority = forms.IntegerField(required=False)
zone = forms.ModelChoiceField(queryset=Zone.objects.all(), required=False)
address = AddressFormField(required=False)
value = forms.CharField(max_length=100, required=False)
class RecordBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Record.objects.all(), widget=forms.MultipleHiddenInput)
def record_zone_choices():
zone_choices = Zone.objects.annotate(record_count=Count('records'))
return [(z.name, '{} ({})'.format(z.name, z.record_count)) for z in zone_choices]
def record_type_choices():
type_choices = {}
records = Record.objects.all()
for r in records:
if not r.record_type in type_choices:
type_choices[r.record_type]=1
else:
type_choices[r.record_type]+=1
return [(t, '{} ({})'.format(t, count)) for t,count in type_choices.items()]
class RecordFilterForm(forms.Form, BootstrapMixin):
zone__name = forms.MultipleChoiceField(required=False, choices=record_zone_choices, label='Zone',
widget=forms.SelectMultiple(attrs={'size': 8}))
record_type = forms.MultipleChoiceField(required=False, choices=record_type_choices, label='Type',
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-19 10:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('ipam', '0004_ipam_vlangroup_uniqueness'),
]
operations = [
migrations.CreateModel(
name='Record',
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)),
('name', models.CharField(max_length=100)),
('record_type', models.CharField(max_length=10)),
('priority', models.PositiveIntegerField(blank=True)),
('value', models.CharField(blank=True, max_length=100)),
('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='records', to='ipam.IPAddress')),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Zone',
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)),
('name', models.CharField(max_length=100)),
('ttl', models.PositiveIntegerField()),
('soa_name', models.CharField(max_length=100)),
('soa_contact', models.CharField(max_length=100)),
('soa_serial', models.CharField(max_length=100)),
('soa_refresh', models.PositiveIntegerField()),
('soa_retry', models.PositiveIntegerField()),
('soa_expire', models.PositiveIntegerField()),
('soa_minimum', models.PositiveIntegerField()),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='record',
name='zone',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='dns.Zone'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-19 10:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dns', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='record',
name='priority',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-21 10:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dns', '0002_auto_20160719_1058'),
]
operations = [
migrations.AlterModelOptions(
name='record',
options={'ordering': ['category']},
),
migrations.AddField(
model_name='record',
name='category',
field=models.CharField(blank=True, max_length=20),
),
migrations.AddField(
model_name='record',
name='description',
field=models.CharField(blank=True, max_length=20),
),
migrations.AddField(
model_name='zone',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-22 08:20
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dns', '0003_auto_20160721_1059'),
]
operations = [
migrations.AlterModelOptions(
name='record',
options={'ordering': ['name']},
),
migrations.RemoveField(
model_name='record',
name='category',
),
migrations.AlterField(
model_name='record',
name='address',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='records', to='ipam.IPAddress'),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-28 08:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dns', '0004_auto_20160722_0820'),
]
operations = [
migrations.AddField(
model_name='zone',
name='bind_changed',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='zone',
name='soa_serial',
field=models.CharField(max_length=10),
),
]

View File

214
netbox/dns/models.py Normal file
View File

@ -0,0 +1,214 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
#from ipam.models import IPAddress
from utilities.models import CreatedUpdatedModel
import time
from django.db.models.signals import pre_delete
from django.dispatch import receiver
import ipam.models
class Zone(CreatedUpdatedModel):
"""
A Zone represents a DNS zone. It contains SOA data but no records, records are represented as Record objects.
"""
name = models.CharField(max_length=100)
ttl = models.PositiveIntegerField()
soa_name = models.CharField(max_length=100)
soa_contact = models.CharField(max_length=100)
soa_serial = models.CharField(max_length=10)
bind_changed = models.BooleanField(default=True)
soa_refresh = models.PositiveIntegerField()
soa_retry = models.PositiveIntegerField()
soa_expire = models.PositiveIntegerField()
soa_minimum = models.PositiveIntegerField()
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['name']
def __unicode__(self):
return self.name
def get_absolute_url(self):
return reverse('dns:zone', args=[self.pk])
def save(self, *args, **kwargs):
self.bind_changed = True
super(Zone, self).save(*args, **kwargs)
def set_bind_changed(self, value):
self.bind_changed = value
super(Zone, self).save()
def update_serial(self):
"""
Each time a record or the zone is modified, the serial is incremented.
"""
current_date = time.strftime('%Y%m%d',time.localtime())
if not self.soa_serial:
self.soa_serial = current_date+'01'
else:
serial_date = self.soa_serial[:8]
serial_num = self.soa_serial[8:]
if serial_date!=current_date:
self.soa_serial = current_date+'01'
else:
serial_num = int(serial_num)
serial_num += 1
if serial_num<10:
self.soa_serial = current_date + '0' + str(serial_num)
else:
self.soa_serial = current_date + str(serial_num)
self.set_bind_changed(False)
def to_csv(self):
return ','.join([
self.name,
str(self.ttl),
self.soa_name,
self.soa_contact,
self.soa_serial,
str(self.soa_refresh),
str(self.soa_retry),
str(self.soa_expire),
str(self.soa_minimum),
self.description,
])
def to_bind(self,records):
if self.bind_changed:
self.update_serial()
bind_records = ''
for r in records:
bind_records += r.to_bind()+'\n'
bind_export = '\n'.join([
'; '+self.name+((' ('+self.description+')') if self.description else ''),
'; gen by netbox ( '+time.strftime('%A %B %d %Y %H:%M:%S',time.localtime())+' ) ',
'',
'$TTL '+str(self.ttl),
self.soa_name.ljust(30)+' IN '+'SOA '+self.soa_contact+' (',
' '+self.soa_serial.ljust(30)+' ; serial',
' '+str(self.soa_refresh).ljust(30)+' ; refresh',
' '+str(self.soa_retry).ljust(30)+' ; retry',
' '+str(self.soa_expire).ljust(30)+' ; expire',
' '+str(self.soa_minimum).ljust(29)+') ; minimum',
'',
'',
'',
])
bind_export += '\n'+bind_records
bind_export += '\n'+'; end '
return bind_export
class Record(CreatedUpdatedModel):
"""
A Record represents a DNS record, i.e. a row in a DNS zone.
"""
name = models.CharField(max_length=100)
record_type = models.CharField(max_length=10)
priority = models.PositiveIntegerField(blank=True, null=True)
zone = models.ForeignKey('Zone', related_name='records', on_delete=models.CASCADE)
address = models.ForeignKey('ipam.IPAddress', related_name='records', on_delete=models.PROTECT, blank=True, null=True)
value = models.CharField(max_length=100, blank=True)
description = models.CharField(max_length=20, blank=True)
class Meta:
ordering = ['name']
def __unicode__(self):
return self.name
def get_absolute_url(self):
return reverse('dns:record', args=[self.pk])
def clean(self):
self.record_type = self.record_type.upper()
if not self.address and not self.value:
raise ValidationError("DNS records must have either an IP address or a text value")
def save(self, *args, **kwargs):
self.zone.save() # in order to update serial.
super(Record, self).save(*args, **kwargs)
def to_csv(self):
return ','.join([
self.zone.name,
self.name,
self.record_type,
str(self.priority) if self.priority else '',
str(self.address) if self.address else '',
self.value,
self.description,
])
def to_bind(self):
return ''.join([
(self.name if self.name!='@' else '').ljust(30),
' IN ',
self.record_type.upper().ljust(10),
' ',
(str(self.priority) if self.priority else '').ljust(4),
' ',
(str(self.address).split('/')[0] if self.address else self.value).ljust(25),
' ',
' ; '+self.description+' ; gen by netbox ( '+time.strftime('%A %B %d %Y %H:%M:%S',time.localtime())+' ) '
])
@receiver(pre_delete, sender=Record)
def on_record_delete(sender, **kwargs):
kwargs['instance'].zone.save()
#
# BIND Exports
#
def export_bind_forward():
zones = Zone.objects.all()
zones_list = []
for z in zones:
records = Record.objects.filter(zone=z)
zones_list.append({
'num': len(zones_list),
'id': z.name,
'content': z.to_bind(records)
})
return zones_list
def export_bind_reverse():
zones = {}
prefixes = ipam.models.Prefix.objects.all()
for p in prefixes:
child_ip = ipam.models.IPAddress.objects.filter(address__net_contained_or_equal=str(p.prefix))
z = p.to_bind(child_ip)
for zz in z:
if not zz['id'] in zones:
zones[zz['id']] = zz['content']
zones_list = []
for zid,zc in zones.items():
zones_list.append({
'num': len(zones_list),
'id': zid,
'content': zc,
})
return zones_list

63
netbox/dns/tables.py Normal file
View File

@ -0,0 +1,63 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn
from ipam.models import IPAddress
from .models import Zone, Record
#
# Zones
#
class ZoneTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dns:zone', args=[Accessor('pk')], verbose_name='Name')
record_count = tables.Column(verbose_name='Records')
ttl = tables.Column(verbose_name='TTL')
soa_name = tables.Column(verbose_name='SOA Name')
soa_contact = tables.Column(verbose_name='SOA Contact')
soa_serial = tables.Column(verbose_name='SOA Serial')
class Meta(BaseTable.Meta):
model = Zone
fields = ('pk', 'name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial')
#
# Records
#
class RecordTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
record_type = tables.Column(verbose_name='Type')
priority = tables.Column(verbose_name='Priority')
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('address.pk')], verbose_name='IP Address')
value = tables.Column(verbose_name='Text Value')
zone = tables.LinkColumn('dns:zone', args=[Accessor('zone.pk')], verbose_name='Zone')
class Meta(BaseTable.Meta):
model=Record
fields = ('pk', 'name', 'record_type', 'priority', 'address', 'value')
class RecordBriefTable(BaseTable):
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
record_type = tables.Column(verbose_name='Type')
priority = tables.Column(verbose_name='Priority')
zone = tables.LinkColumn('dns:zone', args=[Accessor('zone.pk')], verbose_name='Zone')
class Meta(BaseTable.Meta):
model=Record
fields = ('name', 'record_type', 'priority', 'zone')
class RecordZoneTable(BaseTable):
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
record_type = tables.Column(verbose_name='Type')
priority = tables.Column(verbose_name='Priority')
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('address.pk')], verbose_name='IP Address')
value = tables.Column(verbose_name='Value')
class Meta(BaseTable.Meta):
model=Record
fields = ('name', 'record_type', 'priority', 'address', 'value')

3
netbox/dns/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

31
netbox/dns/urls.py Normal file
View File

@ -0,0 +1,31 @@
from django.conf.urls import url
from . import views
urlpatterns = [
# Zones
url(r'^zones/$', views.ZoneListView.as_view(), name='zone_list'),
url(r'^zones/add/$', views.ZoneEditView.as_view(), name='zone_add'),
url(r'^zones/import/$', views.ZoneBulkImportView.as_view(), name='zone_import'),
url(r'^zones/edit/$', views.ZoneBulkEditView.as_view(), name='zone_bulk_edit'),
url(r'^zones/delete/$', views.ZoneBulkDeleteView.as_view(), name='zone_bulk_delete'),
url(r'^zones/(?P<pk>\d+)/$', views.zone, name='zone'),
url(r'^zones/(?P<pk>\d+)/edit/$', views.ZoneEditView.as_view(), name='zone_edit'),
url(r'^zones/(?P<pk>\d+)/delete/$', views.ZoneDeleteView.as_view(), name='zone_delete'),
# Records
url(r'^records/$', views.RecordListView.as_view(), name='record_list'),
url(r'^records/add/$', views.RecordEditView.as_view(), name='record_add'),
url(r'^records/import/$', views.RecordBulkImportView.as_view(), name='record_import'),
url(r'^records/edit/$', views.RecordBulkEditView.as_view(), name='record_bulk_edit'),
url(r'^records/delete/$', views.RecordBulkDeleteView.as_view(), name='record_bulk_delete'),
url(r'^records/(?P<pk>\d+)/$', views.record, name='record'),
url(r'^records/(?P<pk>\d+)/edit/$', views.RecordEditView.as_view(), name='record_edit'),
url(r'^records/(?P<pk>\d+)/delete/$', views.RecordDeleteView.as_view(), name='record_delete'),
# BIND Exports
url(r'^bind/forward/$', views.full_forward, name='full_forward'),
url(r'^bind/reverse/$', views.full_reverse, name='full_reverse'),
]

201
netbox/dns/views.py Normal file
View File

@ -0,0 +1,201 @@
from django_tables2 import RequestConfig
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse
from ipam.models import IPAddress, Prefix
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import Zone, Record, export_bind_forward, export_bind_reverse
from .tables import RecordZoneTable
import StringIO, zipfile, time
#
# Zones
#
class ZoneListView(ObjectListView):
queryset = Zone.objects.annotate(record_count=Count('records'))
filter = filters.ZoneFilter
filter_form = forms.ZoneFilterForm
table = tables.ZoneTable
edit_permissions = ['dns.change_zone', 'dns.delete_zone']
template_name = 'dns/zone_list.html'
def zone(request, pk):
zone = get_object_or_404(Zone.objects.all(), pk=pk)
records = Record.objects.filter(zone=zone)
record_count = len(records)
# DNS records
dns_records = Record.objects.filter(zone=zone)
dns_records_table = RecordZoneTable(dns_records)
return render(request, 'dns/zone.html', {
'zone': zone,
'records': records,
'record_count': record_count,
'dns_records_table': dns_records_table,
})
class ZoneEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dns.change_zone'
model = Zone
form_class = forms.ZoneForm
cancel_url = 'dns:zone_list'
class ZoneDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dns.delete_zone'
model = Zone
redirect_url = 'dns:zone_list'
class ZoneBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dns.add_zone'
form = forms.ZoneImportForm
table = tables.ZoneTable
template_name = 'dns/zone_import.html'
obj_list_url = 'dns:zone_list'
class ZoneBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dns.change_zone'
cls = Zone
form = forms.ZoneBulkEditForm
template_name = 'dns/zone_bulk_edit.html'
default_redirect_url = 'dns:zone_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
zlist = self.cls.objects.filter(pk__in=pk_list)
for z in zlist:
z.set_bind_changed(True)
return zlist.update(**fields_to_update)
class ZoneBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dns.delete_zone'
cls = Zone
form = forms.ZoneBulkDeleteForm
default_redirect_url = 'dns:zone_list'
#
# Records
#
class RecordListView(ObjectListView):
queryset = Record.objects.all()
filter = filters.RecordFilter
filter_form = forms.RecordFilterForm
table = tables.RecordTable
edit_permissions = ['dns.change_record', 'dns.delete_record']
template_name = 'dns/record_list.html'
def record(request, pk):
record = get_object_or_404(Record.objects.all(), pk=pk)
bind_export = record.to_bind()
return render(request, 'dns/record.html', {
'record': record,
'bind_export': bind_export,
})
class RecordEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dns.change_record'
model = Record
form_class = forms.RecordForm
cancel_url = 'dns:record_list'
class RecordDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dns.delete_record'
model = Record
redirect_url = 'dns:record_list'
class RecordBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dns.add_record'
form = forms.RecordImportForm
table = tables.RecordTable
template_name = 'dns/record_import.html'
obj_list_url = 'dns:record_list'
class RecordBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dns.change_record'
cls = Record
form = forms.RecordBulkEditForm
template_name = 'dns/record_bulk_edit.html'
default_redirect_url = 'dns:record_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['name', 'record_type', 'priority', 'zone', 'address', 'value']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
rlist = self.cls.objects.filter(pk__in=pk_list)
if rlist:
rlist[0].save()
return rlist.update(**fields_to_update)
class RecordBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dns.delete_record'
cls = Record
form = forms.RecordBulkEditForm
default_redirect_url = 'dns:record_list'
#
# BIND Exports
#
def bind_export(request, zones_list, context):
download = request.GET.get('download')
if download:
if download == 'all':
zbuf = StringIO.StringIO()
zfile = zipfile.ZipFile(zbuf, mode='w')
temp = []
for z in zones_list:
temp.append(StringIO.StringIO())
temp[len(temp)-1].write(z['content'])
zfile.writestr(z['id'],str(temp[len(temp)-1].getvalue()))
zfile.close()
response = HttpResponse(
zbuf.getvalue(),
content_type = 'application/zip'
)
response['Content-Disposition'] = 'attachment; filename="netbox_dns_{}_{}.zip"'.format(context, str(int(time.time())))
return response
else:
response = HttpResponse(
zones_list[int(download)]['content'],
content_type='text/plain'
)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(zones_list[int(download)]['id'])
return response
else:
return render(request, 'dns/bind_export.html', {
'context': context[0].upper() + context[1:],
'zones': zones_list,
'bind_export_count': len(zones_list),
})
def full_forward(request):
return bind_export(request, export_bind_forward(), 'forward')
def full_reverse(request):
return bind_export(request, export_bind_reverse(), 'reverse')

View File

@ -136,7 +136,7 @@ class PrefixSerializer(serializers.ModelSerializer):
class Meta:
model = Prefix
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']
class PrefixNestedSerializer(PrefixSerializer):
@ -156,7 +156,7 @@ class IPAddressSerializer(serializers.ModelSerializer):
class Meta:
model = IPAddress
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'ptr', 'interface', 'description', 'nat_inside', 'nat_outside']
class IPAddressNestedSerializer(IPAddressSerializer):

View File

@ -42,7 +42,15 @@
"vlan": null,
"status": 1,
"role": 1,
"description": ""
"description": "",
"ttl": 10800,
"soa_name": "@",
"soa_contact": "ns.foo.net. noc.foo.net.",
"soa_serial": "2016070401",
"soa_refresh": 3600,
"soa_retry": 3600,
"soa_expire": 604800,
"soa_minimum": 1800
}
},
{
@ -58,7 +66,15 @@
"vlan": null,
"status": 1,
"role": 1,
"description": ""
"description": "",
"ttl": 10800,
"soa_name": "",
"soa_contact": "",
"soa_serial": "",
"soa_refresh": "",
"soa_retry": "",
"soa_expire": "",
"soa_minimum": ""
}
},
{
@ -70,6 +86,7 @@
"family": 4,
"address": "10.0.255.1/32",
"vrf": null,
"ptr": "www.foo.net",
"interface": 3,
"nat_inside": null,
"description": ""
@ -84,6 +101,7 @@
"family": 4,
"address": "169.254.254.1/31",
"vrf": null,
"ptr": "",
"interface": 4,
"nat_inside": null,
"description": ""
@ -98,6 +116,7 @@
"family": 4,
"address": "10.0.255.2/32",
"vrf": null,
"ptr": "",
"interface": 185,
"nat_inside": null,
"description": ""
@ -112,6 +131,7 @@
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"ptr": "",
"interface": 213,
"nat_inside": null,
"description": ""
@ -126,6 +146,7 @@
"family": 4,
"address": "10.0.254.1/24",
"vrf": null,
"ptr": "",
"interface": 12,
"nat_inside": null,
"description": ""
@ -140,6 +161,7 @@
"family": 4,
"address": "10.15.21.1/31",
"vrf": null,
"ptr": "",
"interface": 218,
"nat_inside": null,
"description": ""
@ -154,6 +176,7 @@
"family": 4,
"address": "10.15.21.2/31",
"vrf": null,
"ptr": "",
"interface": 9,
"nat_inside": null,
"description": ""
@ -168,6 +191,7 @@
"family": 4,
"address": "10.15.22.1/31",
"vrf": null,
"ptr": "",
"interface": 8,
"nat_inside": null,
"description": ""
@ -182,6 +206,7 @@
"family": 4,
"address": "10.15.20.1/31",
"vrf": null,
"ptr": "",
"interface": 7,
"nat_inside": null,
"description": ""
@ -196,6 +221,7 @@
"family": 4,
"address": "10.16.20.1/31",
"vrf": null,
"ptr": "",
"interface": 216,
"nat_inside": null,
"description": ""
@ -210,6 +236,7 @@
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"ptr": "",
"interface": 206,
"nat_inside": null,
"description": ""
@ -224,6 +251,7 @@
"family": 4,
"address": "10.16.22.1/31",
"vrf": null,
"ptr": "",
"interface": 217,
"nat_inside": null,
"description": ""
@ -238,6 +266,7 @@
"family": 4,
"address": "10.16.22.2/31",
"vrf": null,
"ptr": "",
"interface": 205,
"nat_inside": null,
"description": ""
@ -252,6 +281,7 @@
"family": 4,
"address": "10.16.20.2/31",
"vrf": null,
"ptr": "",
"interface": 211,
"nat_inside": null,
"description": ""
@ -266,6 +296,7 @@
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"ptr": "",
"interface": 212,
"nat_inside": null,
"description": ""
@ -280,6 +311,7 @@
"family": 4,
"address": "10.0.254.2/32",
"vrf": null,
"ptr": "",
"interface": 188,
"nat_inside": null,
"description": ""
@ -294,6 +326,7 @@
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"ptr": "",
"interface": 200,
"nat_inside": null,
"description": ""
@ -308,6 +341,7 @@
"family": 4,
"address": "169.254.1.2/31",
"vrf": null,
"ptr": "",
"interface": 194,
"nat_inside": null,
"description": ""

View File

@ -158,7 +158,16 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description', 'ttl', 'soa_name', 'soa_contact', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']
labels = {
'ttl': 'Rev. DNS - TTL',
'soa_name': 'Rev. DNS - SOA Name',
'soa_contact': 'Rev. DNS - SOA Contact',
'soa_refresh': 'Rev. DNS - SOA Refresh',
'soa_retry': 'Rev. DNS - SOA Retry',
'soa_expire': 'Rev. DNS - SOA Expire',
'soa_minimum': 'Rev. DNS - SOA Minimum',
}
help_texts = {
'prefix': "IPv4 or IPv6 network",
'vrf': "VRF (if applicable)",
@ -166,6 +175,13 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
'vlan': "The VLAN to which this prefix is assigned (if applicable)",
'status': "Operational status of this prefix",
'role': "The primary function of this prefix",
'ttl': "Time to live, in seconds",
'soa_name': "The primary name server for the domain, @ for origin",
'soa_contact': "The responsible party for the zone (e.g. ns.foo.net. noc.foo.net.)",
'soa_refresh': "Refresh time, in seconds",
'soa_retry': "Retry time, in seconds",
'soa_expire': "Expire time, in seconds",
'soa_minimum': "Negative result TTL, in seconds",
}
def __init__(self, *args, **kwargs):
@ -212,7 +228,7 @@ class PrefixFromCSVForm(forms.ModelForm):
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
'description']
'description', 'ttl', 'soa_name', 'soa_contact', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']
def clean(self):
@ -264,6 +280,14 @@ class PrefixBulkEditForm(forms.Form, BootstrapMixin):
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
ttl = forms.IntegerField(required=False, label='Reverse DNS - TTL')
soa_name = forms.CharField(max_length=100, required=False, label='Reverse DNS - SOA Name')
soa_contact = forms.CharField(max_length=100, required=False, label='Reverse DNS - SOA Contact')
soa_refresh = forms.IntegerField(required=False, label='Reverse DNS - SOA Refresh')
soa_retry = forms.IntegerField(required=False, label='Reverse DNS - SOA Retry')
soa_expire = forms.IntegerField(required=False, label='Reverse DNS - SOA Expire')
soa_minimum = forms.IntegerField(required=False, label='Reverse DNS - SOA Minimum')
def prefix_vrf_choices():
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
@ -326,10 +350,11 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
fields = ['address', 'vrf', 'tenant', 'ptr', 'nat_device', 'nat_inside', 'description']
help_texts = {
'address': "IPv4 or IPv6 address and mask",
'vrf': "VRF (if applicable)",
'ptr': "Reverse DNS name",
}
def __init__(self, *args, **kwargs):
@ -384,7 +409,8 @@ class IPAddressFromCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'tenant', 'ptr', 'device', 'interface_name', 'is_primary', 'description']
def clean(self):
@ -430,10 +456,12 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
ptr = forms.CharField(max_length=100, required=False, label='PTR')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
description = forms.CharField(max_length=100, required=False)
def ipaddress_family_choices():
return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-19 15:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0004_ipam_vlangroup_uniqueness'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='hostname',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-20 09:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0005_ipaddress_hostname'),
]
operations = [
migrations.AlterField(
model_name='ipaddress',
name='hostname',
field=models.CharField(blank=True, max_length=100, verbose_name=b'Host Name'),
),
]

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-22 09:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0006_auto_20160720_0941'),
]
operations = [
migrations.AddField(
model_name='prefix',
name='soa_contact',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='prefix',
name='soa_expire',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prefix',
name='soa_minimum',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prefix',
name='soa_name',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='prefix',
name='soa_refresh',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prefix',
name='soa_retry',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prefix',
name='soa_serial',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='prefix',
name='ttl',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-27 13:07
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0007_auto_20160722_0958'),
]
operations = [
migrations.RemoveField(
model_name='ipaddress',
name='hostname',
),
migrations.AddField(
model_name='ipaddress',
name='ptr',
field=models.CharField(blank=True, max_length=100, verbose_name=b'PTR'),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-28 08:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0008_auto_20160727_1307'),
]
operations = [
migrations.AddField(
model_name='prefix',
name='bind_changed',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='prefix',
name='soa_serial',
field=models.CharField(blank=True, max_length=10),
),
]

View File

@ -9,9 +9,13 @@ from django.db import models
from dcim.models import Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
import dns.models
from .fields import IPNetworkField, IPAddressField
import time, ipaddress
import netaddr
AF_CHOICES = (
(4, 'IPv4'),
@ -238,10 +242,24 @@ class Prefix(CreatedUpdatedModel):
verbose_name='VLAN')
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
description = models.CharField(max_length=100, blank=True)
objects = PrefixQuerySet.as_manager()
#Reverse DNS
ttl = models.PositiveIntegerField(blank=True, null=True)
soa_name = models.CharField(max_length=100, blank=True)
soa_contact = models.CharField(max_length=100, blank=True)
soa_serial = models.CharField(max_length=10, blank=True)
bind_changed = models.BooleanField(default=True)
soa_refresh = models.PositiveIntegerField(blank=True, null=True)
soa_retry = models.PositiveIntegerField(blank=True, null=True)
soa_expire = models.PositiveIntegerField(blank=True, null=True)
soa_minimum = models.PositiveIntegerField(blank=True, null=True)
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['family', 'prefix']
verbose_name_plural = 'prefixes'
@ -262,6 +280,7 @@ class Prefix(CreatedUpdatedModel):
"instead.")
def save(self, *args, **kwargs):
self.bind_changed = True
if self.prefix:
# Clear host bits from prefix
self.prefix = self.prefix.cidr
@ -269,6 +288,32 @@ class Prefix(CreatedUpdatedModel):
self.family = self.prefix.version
super(Prefix, self).save(*args, **kwargs)
def set_bind_changed(self, value):
self.bind_changed = value
super(Prefix, self).save()
def update_serial(self):
"""
Each time a record or the zone is modified, the serial is incremented.
"""
current_date = time.strftime('%Y%m%d',time.localtime())
if not self.soa_serial:
self.soa_serial = current_date+'01'
else:
serial_date = self.soa_serial[:8]
serial_num = self.soa_serial[8:]
if serial_date!=current_date:
self.soa_serial = current_date+'01'
else:
serial_num = int(serial_num)
serial_num += 1
if serial_num < 10:
self.soa_serial = current_date + '0' + str(serial_num)
else:
self.soa_serial = current_date + str(serial_num)
self.set_bind_changed(False)
def to_csv(self):
return ','.join([
str(self.prefix),
@ -277,6 +322,14 @@ class Prefix(CreatedUpdatedModel):
self.get_status_display(),
self.role.name if self.role else '',
self.description,
self.ttl if self.ttl else '',
self.soa_name if self.soa_name else '',
self.soa_contact if self.soa_contact else '',
self.soa_serial if self.soa_serial else '',
self.soa_refresh if self.soa_refresh else '',
self.soa_retry if self.soa_retry else '',
self.soa_expire if self.soa_expire else '',
self.soa_minimum if self.soa_minimum else '',
])
@property
@ -293,6 +346,121 @@ class Prefix(CreatedUpdatedModel):
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
def to_bind(self,ipaddresses):
if self.bind_changed:
self.update_serial()
zones = {}
def header (zone_id): return '\n'.join([
'; '+zone_id,
'; gen from prefix '+str(self.prefix)+' ('+(self.description if self.description else '')+') by netbox ( '+time.strftime('%A %B %d %Y %H:%M:%S',time.localtime())+' ) ',
'',
'$TTL '+str(self.ttl),
self.soa_name.ljust(30)+' IN '+'SOA '+self.soa_contact+' (',
' '+self.soa_serial.ljust(30)+' ; serial',
' '+str(self.soa_refresh).ljust(30)+' ; refresh',
' '+str(self.soa_retry).ljust(30)+' ; retry',
' '+str(self.soa_expire).ljust(30)+' ; expire',
' '+str(self.soa_minimum).ljust(29)+') ; minimum',
'',
'',
'',
'$ORIGIN '+zone_id,
'',
'',
])
if self.prefix.version == 4:
pbytes = str(self.prefix).split('/')[0].split('.')
pslash = int(str(self.prefix).split('/')[1])
if pslash > 16:
pbytes[3]='0'
zslash = 24
largerPrefix = Prefix.objects.filter(family=4, prefix__net_contains_or_equals=pbytes[0]+'.'+pbytes[1]+'.0.0/16')
if largerPrefix:
pbytes[2]='0'
zslash = 16
else:
pbytes[2]='0'
zslash = 16
if pslash > zslash:
pslash = zslash
p = IPNetwork(unicode('.'.join(pbytes)+'/'+str(pslash)))
ipaddresses = IPAddress.objects.filter(family=4)
for ip in ipaddresses:
if ip.ptr:
ibytes = str(ip.address).split('/')[0].split('.')
islash = str(ip.address).split('/')[1]
i = netaddr.IPAddress(unicode('.'.join(ibytes)))
if i in p:
if zslash == 24:
zone_id = ibytes[2] + '.' + ibytes[1] + '.' + ibytes[0] + '.in-addr.arpa.'
if not zone_id in zones:
zones[zone_id] = header(zone_id)
zones[zone_id] += ibytes[3].ljust(3) + ' IN PTR ' + ip.ptr.ljust(40) + ' ; ' + ip.description.ljust(20) + ' ; gen by netbox ( '+time.strftime('%A %B %d %Y %H:%M:%S',time.localtime())+' ) \n'
else:
zone_id = ibytes[1]+'.'+ibytes[0]+'.in-addr.arpa.'
if not zone_id in zones:
zones[zone_id] = header(zone_id)
zones[zone_id] += (ibytes[3]+'.'+ibytes[2]).ljust(7) + ' IN PTR ' + ip.ptr.ljust(40) + ' ; ' + ip.description.ljust(20) + ' ; gen by netbox ( '+time.strftime('%A %B %d %Y %H:%M:%S',time.localtime())+' ) \n'
else:
pfull = str(ipaddress.IPv6Address(unicode(str(self.prefix).split('/')[0])).exploded)
pnibbles = pfull.split(':')
pdigits = pfull.replace(':','')
pslash = int(str(self.prefix).split('/')[1])
zslash = pslash if pslash % 16 == 0 else pslash/16+16
pnibbles = pnibbles[:zslash/16] + ['0000'] * (8 - zslash/16)
largerPrefix = Prefix.objects.filter(family=6, prefix__net_contains_or_equals=':'.join(pnibbles)+'/'+str(zslash))
if largerPrefix:
#choper le plus grand
minSlash = 128
for pp in largerPrefix:
ppslash = int(str(pp.prefix).split('/')[1])
if ppslash < minSlash:
minSlash = ppslash
zslash = minSlash
pnibbles = pnibbles[:zslash/16] + ['0000'] * (8 - zslash/16)
for ip in ipaddresses:
if ip.ptr:
ifull = str(ipaddress.IPv6Address(unicode(str(ip.address).split('/')[0])).exploded)
inibbles = ifull.split(':')
idigits = ifull.replace(':','')[::-1]
islash = int(str(ip.address).split('/')[1])
pdigitszone = pdigits[:zslash/4][::-1]
zone_id = '.'.join(pdigitszone)+'.ip6.arpa.'
if not zone_id in zones:
zones[zone_id] = header(zone_id)
zones[zone_id] += ('.'.join(idigits[:32-zslash/4])).ljust(30)+' IN PTR ' + ip.ptr.ljust(40) + ' ; ' + ip.description.ljust(20) + ' ; gen by netbox ( '+time.strftime('%A %B %d %Y %H:%M:%S',time.localtime())+' ) \n'
for z in zones:
z += '\n\n; end '
ret = []
for zid,zc in zones.items():
ret.append({
'num': len(ret),
'id': zid,
'content': zc,
})
return ret
class IPAddress(CreatedUpdatedModel):
"""
@ -310,6 +478,7 @@ class IPAddress(CreatedUpdatedModel):
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
ptr = models.CharField(max_length=100, blank=True, verbose_name='PTR')
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@ -343,11 +512,39 @@ class IPAddress(CreatedUpdatedModel):
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
def save(self, *args, **kwargs):
self.update_dns()
if self.address:
# Infer address family from IPAddress object
self.family = self.address.version
dns_records = dns.models.Record.objects.filter(address=self)
for r in dns_records:
r.save()
super(IPAddress, self).save(*args, **kwargs)
def update_dns(self):
"""Auto-create a corresponding A/AAAA DNS record (if possible) whenever the PTR field is modified"""
if self.ptr:
which_zone = None
zones = dns.models.Zone.objects.all()
for zone in zones:
if self.ptr.endswith(zone.name):
which_zone = zone
break
if which_zone:
zone_name = which_zone.name
record_name = self.ptr[:-len(zone_name)]
if record_name.endswith('.'):
record_name = record_name[:-1]
record_type = 'A' if self.family==4 else 'AAAA'
dns.models.Record.objects.get_or_create(
name = record_name,
record_type = record_type,
zone = which_zone,
address = self
)
def to_csv(self):
# Determine if this IP is primary for a Device
@ -360,6 +557,7 @@ class IPAddress(CreatedUpdatedModel):
return ','.join([
str(self.address),
self.vrf.rd if self.vrf else '',
self.ptr if self.ptr else '',
self.device.identifier if self.device else '',
self.interface.name if self.interface else '',
'True' if is_primary else '',

View File

@ -172,6 +172,7 @@ class IPAddressTable(BaseTable):
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
ptr = tables.Column(verbose_name='PTR')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
verbose_name='Device')
interface = tables.Column(orderable=False, verbose_name='Interface')
@ -179,7 +180,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
fields = ('pk', 'address', 'vrf', 'tenant', 'ptr', 'device', 'interface', 'description')
class IPAddressBriefTable(BaseTable):

View File

@ -6,6 +6,8 @@ from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from dcim.models import Device
from dns.models import Zone, Record
from dns.tables import RecordBriefTable
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@ -276,9 +278,10 @@ def prefix(request, pk):
except Aggregate.DoesNotExist:
aggregate = None
child_ip = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))
# Count child IP addresses
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
.count()
ipaddress_count = child_ip.count()
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
@ -355,11 +358,14 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'status', 'role', 'description']:
for field in ['site', 'status', 'role', 'description', 'ttl', 'soa_name', 'soa_contact', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
plist = self.cls.objects.filter(pk__in=pk_list)
for p in plist:
p.set_bind_changed(True)
return plist.update(**fields_to_update)
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@ -419,11 +425,16 @@ def ipaddress(request, pk):
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
related_ips_table = tables.IPAddressBriefTable(related_ips)
# Related DNS records
dns_records = Record.objects.filter(address=ipaddress)
dns_records_table = RecordBriefTable(dns_records)
return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress,
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
'related_ips_table': related_ips_table,
'dns_records_table': dns_records_table,
})
@ -480,11 +491,14 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['description']:
for field in ['ptr', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
iplist = self.cls.objects.filter(pk__in=pk_list)
for ip in iplist:
ip.save()
return iplist.update(**fields_to_update)
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):

View File

@ -106,6 +106,7 @@ INSTALLED_APPS = (
'circuits',
'dcim',
'ipam',
'dns',
'extras',
'secrets',
'tenancy',

View File

@ -21,6 +21,7 @@ urlpatterns = [
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
url(r'^dns/', include('dns.urls', namespace='dns')),
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
url(r'^profile/', include('users.urls', namespace='users')),
@ -29,6 +30,7 @@ urlpatterns = [
url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')),
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
url(r'^api/dns/', include('dns.api.urls', namespace='dns-api')),
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
url(r'^api/docs/', include('rest_framework_swagger.urls')),

View File

@ -6,6 +6,7 @@ from circuits.models import Provider, Circuit
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
from extras.models import UserAction
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
from dns.models import Zone, Record
from secrets.models import Secret
from tenancy.models import Tenant
@ -32,6 +33,10 @@ def home(request):
'ipaddress_count': IPAddress.objects.count(),
'vlan_count': VLAN.objects.count(),
# DNS
'zone_count': Zone.objects.count(),
'record_count': Record.objects.count(),
# Circuits
'provider_count': Provider.objects.count(),
'circuit_count': Circuit.objects.count(),

View File

@ -9,7 +9,7 @@ html, body {
height: 100%;
}
body {
padding-top: 70px;
padding-top: 60px;
}
.container {
width: auto;
@ -19,7 +19,7 @@ body {
min-height: 100%;
height: auto !important;
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
padding-bottom: 30px;
padding-bottom: 5px;
}
.footer, .push {
height: 60px; /* .push must be the same height as .footer */

View File

@ -165,6 +165,25 @@
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/dns/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">DNS <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dns:record_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Records</a></li>
{% if perms.dns.add_record %}
<li><a href="{% url 'dns:record_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a record</a></li>
<li><a href="{% url 'dns:record_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import records</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dns:zone_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Zones</a></li>
{% if perms.dns.add_zone %}
<li><a href="{% url 'dns:zone_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a zone</a></li>
<li><a href="{% url 'dns:zone_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import zones</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dns:full_forward' %}"><i class="glyphicon glyphicon-export" aria-hidden="true"></i> BIND Export Forward</a></li>
<li><a href="{% url 'dns:full_reverse' %}"><i class="glyphicon glyphicon-export" aria-hidden="true"></i> BIND Export Reverse</a></li>
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu">

View File

@ -0,0 +1,64 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}BIND Export {{context}}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}download=all" class="btn btn-success pull-right">
<span class="glyphicon glyphicon-export" aria-hidden="true"></span>
Download ZIP
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1 style="margin-bottom: 35px;">BIND Export {{context}}</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
{% for z in zones %}
<div class="panel panel-default" style="box-sizing: content-box; max-width: 100%; overflow: scroll;">
<div class="panel-heading">
<strong class="text-md-left">{{ z.id }}</strong>
<span class="pull-right">
<a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}download={{ z.num }}">Download</a>
-
<a id="bind_export_select_{{ z.num }}" href="#">Select</a>
</span>
</div>
<table class="table table-hover panel-body">
<tr><td>
<pre id="bind_export_{{ z.num }}" style="overflow: auto; overflow-x: auto; overflow-y: auto; word-wrap: break-word; white-space: pre;">{{ z.content }}</pre>
</td></tr>
</table>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script>
for(var i=0;i<{{ bind_export_count }};i++) {
$('#bind_export_select_'+i).click(function(e){
var i_str = $(this).attr('id');
i_str = i_str.substr(i_str.lastIndexOf('_')+1);
e.preventDefault();
if(document.selection) {
var range = document.body.createTextRange();
var id='bind_export_'+i_str;
range.moveToElementText(document.getElementById(id));
range.select();
}
else if(window.getSelection) {
var range = document.createRange();
var id='bind_export_'+i_str;
range.selectNode(document.getElementById(id));
window.getSelection().addRange(range);
}
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,147 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Record {{ record }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dns:zone_list' %}">Zones</a></li>
<li><a href="{% url 'dns:zone' pk=record.zone.pk %}">{{ record.zone }}</a></li>
<li><a href="{% url 'dns:record_list' %}">Records</a></li>
<li>{{ record }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'dns:record_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Record" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
{% if perms.dns.change_record %}
<a href="{% url 'dns:record_edit' pk=record.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
Edit this record
</a>
{% endif %}
{% if perms.dns.delete_record %}
<a href="{% url 'dns:record_delete' pk=record.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
Delete this record
</a>
{% endif %}
</div>
<h1>{{ record }}</h1>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Record</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Zone</td>
<td><a href="{% url 'dns:zone' pk=record.zone.pk %}">{{ record.zone }}</a></td>
</tr>
<tr>
<td>Name</td>
<td>{{ record.name }}</td>
</tr>
<tr>
<td>Type</td>
<td>{{ record.record_type }}</td>
</tr>
<tr>
<td>Priority</td>
<td>
{% if record.priority %}
{{ record.priority }}
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>IP Address</td>
<td>
{% if record.address %}
<a href="{% url 'ipam:ipaddress' pk=record.address.pk %}">{{ record.address }}</a>
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>Value</td>
<td>
{% if record.value %}
{{ record.value }}
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if record.description %}
{{ record.description }}
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ record.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ record.last_updated }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong class="text-md-left">BIND Export</strong>
<a class="pull-right" id="bind_export_select" href="#">Select</a>
</div>
<table class="table table-hover panel-body">
<tr>
<td><pre id="bind_export" style="overflow: auto; word-wrap: normal; white-space: pre;">{{ bind_export }}</pre></td>
</tr>
</table>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script>
$('#bind_export_select').click(function(e){
e.preventDefault();
if(document.selection) {
var range = document.body.createTextRange();
range.moveToElementText(document.getElementById('bind_export'));
range.select();
}
else if(window.getSelection) {
var range = document.createRange();
range.selectNode(document.getElementById('bind_export'));
window.getSelection().addRange(range);
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Record Bulk Edit{% endblock %}
{% block select_objects_table %}
{% for record in selected_objects %}
<tr>
<td><a href="{% url 'dns:record' pk=record.pk %}">{{ record.name }}</a></td>
<td>{{ record.record_type }}</td>
<td>{{ record.priority }}</td>
<td>{{ record.address }}</td>
<td>{{ record.value }}</td>
<td>{{ record.zone }}</td>
<td>{{ record.description }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,72 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Record Import{% endblock %}
{% block content %}
<h1>Record Import</h1>
<div class="row">
<div class="col-md-6">
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zone name</td>
<td>Name of the zone the record belongs to</td>
<td>foo.net</td>
</tr>
<tr>
<td>Name</td>
<td>Host name, @ for origin</td>
<td>www</td>
</tr>
<tr>
<td>Type</td>
<td>Record type</td>
<td>AAAA</td>
</tr>
<tr>
<td>Priority</td>
<td>Priority level (optional)</td>
<td>30</td>
</tr>
<tr>
<td>Address</td>
<td>IP address if value is an IP address, in AAAA records for instance</td>
<td>192.168.1.110/16</td>
</tr>
<tr>
<td>Value</td>
<td>Text value else, in CNAME records for instance</td>
<td>foo.net</td>
</tr>
<tr>
<td>Description</td>
<td>Description (optional)</td>
<td>Backend API server</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>foo.net,www,AAAA,,192.168.1.110/16,,Backend API server</pre>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends '_base.html' %}
{% load helpers %}
{% load form_helpers %}
{% block title %}Records{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dns.add_record %}
<a href="{% url 'dns:record_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a record
</a>
<a href="{% url 'dns:record_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
Import records
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='Records' %}
</div>
<h1>Records</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dns:record_bulk_edit' bulk_delete_url='dns:record_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'dns:record_list' %}" method="get">
<div class="input-group">
<input type="text" name="name_or_value_or_ip" class="form-control" placeholder="Name, value or IP" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,116 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Zone {{ zone }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dns:zone_list' %}">Zones</a></li>
<li>{{ zone }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'dns:zone_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Zone" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
{% if perms.dns.change_zone %}
<a href="{% url 'dns:zone_edit' pk=zone.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
Edit this zone
</a>
{% endif %}
{% if perms.dns.delete_zone %}
<a href="{% url 'dns:zone_delete' pk=zone.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
Delete this zone
</a>
{% endif %}
</div>
<h1>{{ zone }}</h1>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Zone</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Name</td>
<td>{{ zone.name }}</td>
</tr>
<tr>
<td>Records</td>
<td>{{ record_count }}</td>
</tr>
<tr>
<td>TTL</td>
<td>{{ zone.ttl }}</td>
</tr>
<tr>
<td>SOA Name</td>
<td>{{ zone.soa_name }}</td>
</tr>
<tr>
<td>SOA Contact</td>
<td>{{ zone.soa_contact }}</td>
</tr>
<tr>
<td>SOA Serial</td>
<td>{{ zone.soa_serial }}</td>
</tr>
<tr>
<td>SOA Refresh</td>
<td>{{ zone.soa_refresh }}</td>
</tr>
<tr>
<td>SOA Retry</td>
<td>{{ zone.soa_retry }}</td>
</tr>
<tr>
<td>SOA Expire</td>
<td>{{ zone.soa_expire }}</td>
</tr>
<tr>
<td>SOA Minimum</td>
<td>{{ zone.soa_minimum }}</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if zone.description %}
{{ zone.description }}
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ zone.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ zone.last_updated }}</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
{% with heading='Records' %}
{% render_table dns_records_table 'panel_table.html' %}
{% endwith %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Zone Bulk Edit{% endblock %}
{% block select_objects_table %}
{% for zone in selected_objects %}
<tr>
<td><a href="{% url 'dns:zone' pk=zone.pk %}">{{ zone.name }}</a></td>
<td>{{ zone.ttl }}</td>
<td>{{ zone.soa_name }}</td>
<td>{{ zone.soa_contact }}</td>
<td>{{ zone.soa_refresh }}</td>
<td>{{ zone.soa_retry }}</td>
<td>{{ zone.soa_expire }}</td>
<td>{{ zone.soa_minimum }}</td>
<td>{{ zone.description }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Zone Import{% endblock %}
{% block content %}
<h1>Zone Import</h1>
<div class="row">
<div class="col-md-6">
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Name of zone</td>
<td>foo.net</td>
</tr>
<tr>
<td>TTL</td>
<td>Time to live, in seconds</td>
<td>10800</td>
</tr>
<tr>
<td>SOA Name</td>
<td>The primary name server for the domain, @ for origin</td>
<td>@</td>
</tr>
<tr>
<td>SOA Contact</td>
<td>The responsible party for the domain</td>
<td>ns.foo.net. noc.foo.net.</td>
</tr>
<tr>
<td>SOA Refresh</td>
<td>Refresh time, in seconds</td>
<td>3600</td>
</tr>
<tr>
<td>SOA Retry</td>
<td>Retry time, in seconds</td>
<td>3600</td>
</tr>
<tr>
<td>SOA Expire</td>
<td>Expire time, in seconds</td>
<td>604800</td>
</tr>
<tr>
<td>SOA Minimum</td>
<td>Negative result TTL, in seconds</td>
<td>1800</td>
</tr>
<tr>
<td>Description</td>
<td>Description (optional)</td>
<td>Mail servers zone</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>foo.net,10800,@,ns.foo.net. noc.foo.net.,3600,3600,604800,1800,Mail servers zone</pre>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends '_base.html' %}
{% load helpers %}
{% load form_helpers %}
{% block title %}Zones{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dns.add_zone %}
<a href="{% url 'dns:zone_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a zone
</a>
<a href="{% url 'dns:zone_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
Import zones
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='Zones' %}
</div>
<h1>Zones</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dns:zone_bulk_edit' bulk_delete_url='dns:zone_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'dns:zone_list' %}" method="get">
<div class="input-group">
<input type="text" name="name" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -127,18 +127,26 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Circuits</strong>
<strong>DNS</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.provider_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'circuits:provider_list' %}">Providers</a></h4>
<p class="list-group-item-text text-muted">Organizations which provide circuit connectivity</p>
<h4 class="list-group-item-heading"><a href="{% url 'dns:full_forward' %}">Export Forward</a></h4>
<p class="list-group-item-text text-muted">Export forward zones into Bind format</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.circuit_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'circuits:circuit_list' %}">Circuits</a></h4>
<p class="list-group-item-text text-muted">Communication links for Internet transit, peering, and other services</p>
<h4 class="list-group-item-heading"><a href="{% url 'dns:full_reverse' %}">Export Reverse</a></h4>
<p class="list-group-item-text text-muted">Export reverse zones into Bind format</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.zone_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dns:zone_list' %}">Zones</a></h4>
<p class="list-group-item-text text-muted">Domain name system zones</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.record_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dns:record_list' %}">Records</a></h4>
<p class="list-group-item-text text-muted">Links between a hostname and a resource</p>
</div>
</div>
</div>

View File

@ -76,6 +76,16 @@
{% endif %}
</td>
</tr>
<tr>
<td>PTR</td>
<td>
{% if ipaddress.ptr %}
<span>{{ ipaddress.ptr }}</span>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
@ -142,6 +152,9 @@
{% with heading='Related IP Addresses' %}
{% render_table related_ips_table 'panel_table.html' %}
{% endwith %}
{% with heading='Related DNS Records' %}
{% render_table dns_records_table 'panel_table.html' %}
{% endwith %}
</div>
</div>
{% endblock %}

View File

@ -9,6 +9,7 @@
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
<td>{{ ipaddress.vrf|default:"Global" }}</td>
<td>{{ ipaddress.tenant }}</td>
<td>{{ ipaddress.ptr }}</td>
<td>{{ ipaddress.interface.device }}</td>
<td>{{ ipaddress.interface }}</td>
<td>{{ ipaddress.description }}</td>

View File

@ -9,6 +9,7 @@
{% render_field form.address %}
{% render_field form.vrf %}
{% render_field form.tenant %}
{% render_field form.ptr %}
{% if obj %}
<div class="form-group">
<label class="col-md-3 control-label">Device</label>

View File

@ -43,6 +43,11 @@
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>PTR</td>
<td>Reverse DNS Name</td>
<td>foo.com</td>
</tr>
<tr>
<td>Device</td>
<td>Device name (optional)</td>
@ -66,7 +71,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP</pre>
<pre>192.0.2.42/24,65000:123,ABC01,foo.com,switch12,ge-0/0/31,True,Management IP</pre>
</div>
</div>
{% endblock %}

View File

@ -99,6 +99,38 @@
<td>IP Addresses</td>
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }}</a></td>
</tr>
<tr>
<td>Reverse DNS - TTL</td>
<td>{{ prefix.ttl }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Name</td>
<td>{{ prefix.soa_name }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Contact</td>
<td>{{ prefix.soa_contact }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Serial</td>
<td>{{ prefix.soa_serial }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Refresh</td>
<td>{{ prefix.soa_refresh }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Retry</td>
<td>{{ prefix.soa_retry }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Expire</td>
<td>{{ prefix.soa_expire }}</td>
</tr>
<tr>
<td>Reverse DNS - SOA Minimum</td>
<td>{{ prefix.soa_minimum }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ prefix.created }}</td>
@ -133,3 +165,4 @@
</div>
</div>
{% endblock %}

View File

@ -13,6 +13,14 @@
<td>{{ prefix.status }}</td>
<td>{{ prefix.role }}</td>
<td>{{ prefix.description }}</td>
<td>{{ prefix.ttl }}</td>
<td>{{ prefix.soa_name }}</td>
<td>{{ prefix.soa_contact }}</td>
<td>{{ prefix.soa_refresh }}</td>
<td>{{ prefix.soa_retry }}</td>
<td>{{ prefix.soa_expire }}</td>
<td>{{ prefix.soa_minimum }}</td>
<td>{{ prefix.description }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@ -73,10 +73,45 @@
<td>Short description (optional)</td>
<td>7th floor WiFi</td>
</tr>
<tr>
<td>Reverse DNS - TTL</td>
<td>Time to live, in seconds</td>
<td>10800</td>
</tr>
<tr>
<td>Reverse DNS - SOA Name</td>
<td>The primary name server for the domain, @ for origin</td>
<td>@</td>
</tr>
<tr>
<td>Reverse DNS - SOA Contact</td>
<td>The responsible party for the domain</td>
<td>ns.foo.net. noc.foo.net.</td>
</tr>
<tr>
<td>Reverse DNS - SOA Refresh</td>
<td>Refresh time, in seconds</td>
<td>3600</td>
</tr>
<tr>
<td>Reverse DNS - SOA Retry</td>
<td>Retry time, in seconds</td>
<td>3600</td>
</tr>
<tr>
<td>Reverse DNS - SOA Expire</td>
<td>Expire time, in seconds</td>
<td>604800</td>
</tr>
<tr>
<td>Reverse DNS - SOA Minimum</td>
<td>Negative result TTL, in seconds</td>
<td>1800</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi,10800,@,ns.foo.net. noc.foo.net.,3600,3600,604800,1800</pre>
</div>
</div>
{% endblock %}