Remove category field in record & add auto-updating serial in zone

This commit is contained in:
rdujardin 2016-07-22 12:28:19 +02:00
parent efe61886a0
commit 117bf1a118
12 changed files with 99 additions and 66 deletions

View File

@ -4,8 +4,12 @@ The DNS component of NetBox deals with the management of DNS 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.
As zones are readable through the REST API, it is possible to write some external script which automatically generates zone files for a DNS server,
this feature is not directly provided by NetBox though.
The SOA Serial field is automatically created and updated each time something changes in the zone, i.e. each time you edit IP addresses or records
belonging to the zone, or the zone itself. It's in the following format : YYYYMMDDN with Y the year, M the month, D the day and N a counter.
Every zone can be exported as a zone file in BIND format, directly readable by a DNS server. As zones are readable through the REST API,
with a field containing their BIND format, it is possible to write an external script which automatically updates a DNS server
configuration from the Netbox database.
---

View File

@ -6,7 +6,7 @@ from .models import (
Zone,
Record,
)
from .forms import record_type_choices, record_category_choices
from .forms import record_type_choices
class ZoneFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
@ -32,11 +32,6 @@ class RecordFilter(django_filters.FilterSet):
label = 'Type',
choices = record_type_choices
)
cateogry = django_filters.MultipleChoiceFilter(
name = 'category',
label = 'Category',
choices = record_category_choices
)
name = django_filters.CharFilter(
name = 'name',
lookup_type = 'icontains',
@ -48,7 +43,7 @@ class RecordFilter(django_filters.FilterSet):
class Meta:
model=Record
field = ['name', 'record_type', 'value', 'category']
field = ['name', 'record_type', 'value']
def filter_name_or_value(self, queryset, value):
if not value:

View File

@ -19,11 +19,10 @@ class ZoneForm(forms.ModelForm, BootstrapMixin):
class Meta:
model=Zone
fields = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum', 'description']
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_serial': 'SOA Serial',
'soa_refresh': 'SOA Refresh',
'soa_retry': 'SOA Retry',
'soa_expire': 'SOA Expire',
@ -34,7 +33,6 @@ class ZoneForm(forms.ModelForm, BootstrapMixin):
'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_serial': "Serial string in SOA record (e.g. 2016071401)",
'soa_refresh': "Refresh time, in seconds",
'soa_retry': "Retry time, in seconds",
'soa_expire': "Expire time, in seconds",
@ -45,7 +43,7 @@ class ZoneFromCSVForm(forms.ModelForm):
class Meta:
model=Zone
fields = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum', 'description']
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)
@ -77,14 +75,13 @@ class RecordForm(forms.ModelForm, BootstrapMixin):
class Meta:
model=Record
fields = ['name', 'category', 'record_type', 'priority', 'zone', 'address', 'value', 'description']
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)',
'category': 'Category (e.g. SLA, Server or Customer)',
'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',
@ -98,7 +95,15 @@ class RecordFromCSVForm(forms.ModelForm):
class Meta:
model=Record
fields = ['zone', 'name', 'category', 'record_type', 'priority', 'address', 'value', 'description']
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)
@ -106,7 +111,6 @@ class RecordImportForm(BulkImportForm, BootstrapMixin):
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')
category = forms.CharField(max_length=100, required=False, label='Category')
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)
@ -130,22 +134,9 @@ def record_type_choices():
type_choices[r.record_type]+=1
return [(t, '{} ({})'.format(t, count)) for t,count in type_choices.items()]
def record_category_choices():
category_choices = {}
records = Record.objects.all()
for r in records:
if r.category:
if not r.category in category_choices:
category_choices[r.category]=1
else:
category_choices[r.category]+=1
return [(c, '{} ({})'.format(c, count)) for c,count in category_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}))
category = forms.MultipleChoiceField(required=False, choices=record_category_choices, label='Category',
widget=forms.SelectMultiple(attrs={'size': 8}))

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

@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from ipam.models import IPAddress
#from ipam.models import IPAddress
from utilities.models import CreatedUpdatedModel
import time
@ -33,6 +33,29 @@ class Zone(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('dns:zone', args=[self.pk])
def save(self, *args, **kwargs):
self.update_serial()
super(Zone, self).save(*args, **kwargs)
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+'1'
else:
serial_date = self.soa_serial[:8]
serial_num = self.soa_serial[8:]
if serial_date!=current_date:
self.soa_serial = current_date+'1'
else:
serial_num = int(serial_num)
serial_num += 1
self.soa_serial = current_date + str(serial_num)
def to_csv(self):
return ','.join([
self.name,
@ -80,13 +103,12 @@ class Record(CreatedUpdatedModel):
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.SET_NULL, blank=True, null=True)
address = models.ForeignKey('ipam.IPAddress', related_name='records', on_delete=models.PROTECT, blank=True, null=True)
value = models.CharField(max_length=100, blank=True)
category = models.CharField(max_length=20, blank=True)
description = models.CharField(max_length=20, blank=True)
class Meta:
ordering = ['category']
ordering = ['name']
def __unicode__(self):
return self.name
@ -95,15 +117,23 @@ class Record(CreatedUpdatedModel):
return reverse('dns:record', args=[self.pk])
def clean(self):
record_type = record_type.upper()
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)
#POST_DELETE RECEIVER !!!
def delete(self, *args, **kwargs):
self.zone.save() # in order to update serial.
super(Record, self).delete(*args, **kwargs)
def to_csv(self):
return ','.join([
self.zone.name,
self.name,
self.category,
self.record_type,
str(self.priority) if self.priority else '',
str(self.address) if self.address else '',

View File

@ -30,7 +30,6 @@ class ZoneTable(BaseTable):
class RecordTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
category = tables.Column(verbose_name='Category')
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')
@ -39,16 +38,15 @@ class RecordTable(BaseTable):
class Meta(BaseTable.Meta):
model=Record
fields = ('pk', 'name', 'category', 'record_type', 'priority', 'address', 'value')
fields = ('pk', 'name', 'record_type', 'priority', 'address', 'value')
class RecordBriefTable(BaseTable):
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
category = tables.Column(verbose_name='Category')
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', 'category', 'record_type', 'priority', 'zone')
fields = ('name', 'record_type', 'priority', 'zone')

View File

@ -83,7 +83,10 @@ class ZoneBulkEditView(PermissionRequiredMixin, BulkEditView):
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)
zlist = self.cls.objects.filter(pk__in=pk_list)
for z in zlist:
z.save()
return zlist.update(**fields_to_update)
class ZoneBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@ -146,7 +149,10 @@ class RecordBulkEditView(PermissionRequiredMixin, BulkEditView):
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)
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'

View File

@ -56,15 +56,6 @@
<td>Name</td>
<td>{{ record.name }}</td>
</tr>
<tr>
<td>Category</td>
<td>
{% if record.category %}
{{ record.category }}
{% else %}
-
{% endif %}
</td>
<tr>
<td>Type</td>
<td>{{ record.record_type }}</td>
@ -130,7 +121,7 @@
</div>
<table class="table table-hover panel-body">
<tr>
<td><pre id="bind_export" style="overflow: scroll;">{{ bind_export }}</pre></td>
<td><p style="text-overflow: scroll;"><pre id="bind_export">{{ bind_export }}</pre></p></td>
</tr>
</table>
</div>

View File

@ -7,7 +7,6 @@
{% for record in selected_objects %}
<tr>
<td><a href="{% url 'dns:record' pk=record.pk %}">{{ record.name }}</a></td>
<td>{{ record.category }}</td>
<td>{{ record.record_type }}</td>
<td>{{ record.priority }}</td>
<td>{{ record.address }}</td>

View File

@ -38,11 +38,6 @@
<td>Host name, @ for origin</td>
<td>www</td>
</tr>
<tr>
<td>Category</td>
<td>Category (e.g. SLA, Server or Customer ; optional)</td>
<td>Server</td>
</tr>
<tr>
<td>Type</td>
<td>Record type</td>
@ -71,7 +66,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>foo.net,www,Server,AAAA,,192.168.1.110/16,,Backend API server</pre>
<pre>foo.net,www,AAAA,,192.168.1.110/16,,Backend API server</pre>
</div>
</div>
{% endblock %}

View File

@ -162,7 +162,7 @@
</div>
<table class="table table-hover panel-body">
<tr>
<td><pre id="bind_export" style="overflow: scroll;">{{ bind_export }}</pre></td>
<td><p style="text-overflow: scroll;"><pre id="bind_export">{{ bind_export }}</pre></p></td>
</tr>
</table>
</div>

View File

@ -48,11 +48,6 @@
<td>The responsible party for the domain</td>
<td>ns.foo.net. noc.foo.net.</td>
</tr>
<tr>
<td>SOA Serial</td>
<td>Serial string in SOA record</td>
<td>2016070401</td>
</tr>
<tr>
<td>SOA Refresh</td>
<td>Refresh time, in seconds</td>
@ -81,7 +76,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>foo.net,10800,@,ns.foo.net. noc.foo.net.,2016070401,3600,3600,604800,1800,Mail servers zone</pre>
<pre>foo.net,10800,@,ns.foo.net. noc.foo.net.,3600,3600,604800,1800,Mail servers zone</pre>
</div>
</div>
{% endblock %}