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. 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, 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
this feature is not directly provided by NetBox though. 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, Zone,
Record, Record,
) )
from .forms import record_type_choices, record_category_choices from .forms import record_type_choices
class ZoneFilter(django_filters.FilterSet): class ZoneFilter(django_filters.FilterSet):
name = django_filters.CharFilter( name = django_filters.CharFilter(
@ -32,11 +32,6 @@ class RecordFilter(django_filters.FilterSet):
label = 'Type', label = 'Type',
choices = record_type_choices choices = record_type_choices
) )
cateogry = django_filters.MultipleChoiceFilter(
name = 'category',
label = 'Category',
choices = record_category_choices
)
name = django_filters.CharFilter( name = django_filters.CharFilter(
name = 'name', name = 'name',
lookup_type = 'icontains', lookup_type = 'icontains',
@ -48,7 +43,7 @@ class RecordFilter(django_filters.FilterSet):
class Meta: class Meta:
model=Record model=Record
field = ['name', 'record_type', 'value', 'category'] field = ['name', 'record_type', 'value']
def filter_name_or_value(self, queryset, value): def filter_name_or_value(self, queryset, value):
if not value: if not value:

View File

@ -19,11 +19,10 @@ class ZoneForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model=Zone 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 = { labels = {
'soa_name': 'SOA Name', 'soa_name': 'SOA Name',
'soa_contact': 'SOA Contact', 'soa_contact': 'SOA Contact',
'soa_serial': 'SOA Serial',
'soa_refresh': 'SOA Refresh', 'soa_refresh': 'SOA Refresh',
'soa_retry': 'SOA Retry', 'soa_retry': 'SOA Retry',
'soa_expire': 'SOA Expire', 'soa_expire': 'SOA Expire',
@ -34,7 +33,6 @@ class ZoneForm(forms.ModelForm, BootstrapMixin):
'ttl': "Time to live, in seconds", 'ttl': "Time to live, in seconds",
'soa_name': "The primary name server for the domain, @ for origin", '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_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_refresh': "Refresh time, in seconds",
'soa_retry': "Retry time, in seconds", 'soa_retry': "Retry time, in seconds",
'soa_expire': "Expire time, in seconds", 'soa_expire': "Expire time, in seconds",
@ -45,7 +43,7 @@ class ZoneFromCSVForm(forms.ModelForm):
class Meta: class Meta:
model=Zone 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): class ZoneImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=ZoneFromCSVForm) csv = CSVDataField(csv_form=ZoneFromCSVForm)
@ -77,14 +75,13 @@ class RecordForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model=Record model=Record
fields = ['name', 'category', 'record_type', 'priority', 'zone', 'address', 'value', 'description'] fields = ['name', 'record_type', 'priority', 'zone', 'address', 'value', 'description']
labels = { labels = {
'record_type': 'Type', 'record_type': 'Type',
} }
help_texts = { help_texts = {
'name': 'Host name, @ for origin (e.g. www)', 'name': 'Host name, @ for origin (e.g. www)',
'record_type': 'Record type (e.g. MX or AAAA)', 'record_type': 'Record type (e.g. MX or AAAA)',
'category': 'Category (e.g. SLA, Server or Customer)',
'priority': 'Priority level (e.g. 10)', 'priority': 'Priority level (e.g. 10)',
'zone': 'Zone the record belongs to', 'zone': 'Zone the record belongs to',
'address': 'IP address if value is an IP address, in AAAA records for instance', 'address': 'IP address if value is an IP address, in AAAA records for instance',
@ -98,7 +95,15 @@ class RecordFromCSVForm(forms.ModelForm):
class Meta: class Meta:
model=Record 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): class RecordImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=RecordFromCSVForm) csv = CSVDataField(csv_form=RecordFromCSVForm)
@ -106,7 +111,6 @@ class RecordImportForm(BulkImportForm, BootstrapMixin):
class RecordBulkEditForm(forms.Form, BootstrapMixin): class RecordBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Record.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Record.objects.all(), widget=forms.MultipleHiddenInput)
name = forms.CharField(max_length=100, required=False, label='Name') 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') record_type = forms.CharField(max_length=100, required=False, label='Type')
priority = forms.IntegerField(required=False) priority = forms.IntegerField(required=False)
zone = forms.ModelChoiceField(queryset=Zone.objects.all(), 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 type_choices[r.record_type]+=1
return [(t, '{} ({})'.format(t, count)) for t,count in type_choices.items()] 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): class RecordFilterForm(forms.Form, BootstrapMixin):
zone__name = forms.MultipleChoiceField(required=False, choices=record_zone_choices, label='Zone', zone__name = forms.MultipleChoiceField(required=False, choices=record_zone_choices, label='Zone',
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
record_type = forms.MultipleChoiceField(required=False, choices=record_type_choices, label='Type', record_type = forms.MultipleChoiceField(required=False, choices=record_type_choices, label='Type',
widget=forms.SelectMultiple(attrs={'size': 8})) 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.core.urlresolvers import reverse
from django.db import models from django.db import models
from ipam.models import IPAddress #from ipam.models import IPAddress
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
import time import time
@ -33,6 +33,29 @@ class Zone(CreatedUpdatedModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dns:zone', args=[self.pk]) 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): def to_csv(self):
return ','.join([ return ','.join([
self.name, self.name,
@ -80,13 +103,12 @@ class Record(CreatedUpdatedModel):
record_type = models.CharField(max_length=10) record_type = models.CharField(max_length=10)
priority = models.PositiveIntegerField(blank=True, null=True) priority = models.PositiveIntegerField(blank=True, null=True)
zone = models.ForeignKey('Zone', related_name='records', on_delete=models.CASCADE) 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) value = models.CharField(max_length=100, blank=True)
category = models.CharField(max_length=20, blank=True)
description = models.CharField(max_length=20, blank=True) description = models.CharField(max_length=20, blank=True)
class Meta: class Meta:
ordering = ['category'] ordering = ['name']
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@ -95,15 +117,23 @@ class Record(CreatedUpdatedModel):
return reverse('dns:record', args=[self.pk]) return reverse('dns:record', args=[self.pk])
def clean(self): def clean(self):
record_type = record_type.upper() self.record_type = self.record_type.upper()
if not self.address and not self.value: if not self.address and not self.value:
raise ValidationError("DNS records must have either an IP address or a text 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): def to_csv(self):
return ','.join([ return ','.join([
self.zone.name, self.zone.name,
self.name, self.name,
self.category,
self.record_type, self.record_type,
str(self.priority) if self.priority else '', str(self.priority) if self.priority else '',
str(self.address) if self.address else '', str(self.address) if self.address else '',

View File

@ -30,7 +30,6 @@ class ZoneTable(BaseTable):
class RecordTable(BaseTable): class RecordTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name') name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
category = tables.Column(verbose_name='Category')
record_type = tables.Column(verbose_name='Type') record_type = tables.Column(verbose_name='Type')
priority = tables.Column(verbose_name='Priority') priority = tables.Column(verbose_name='Priority')
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('address.pk')], verbose_name='IP Address') address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('address.pk')], verbose_name='IP Address')
@ -39,16 +38,15 @@ class RecordTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model=Record model=Record
fields = ('pk', 'name', 'category', 'record_type', 'priority', 'address', 'value') fields = ('pk', 'name', 'record_type', 'priority', 'address', 'value')
class RecordBriefTable(BaseTable): class RecordBriefTable(BaseTable):
name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name') name = tables.LinkColumn('dns:record', args=[Accessor('pk')], verbose_name='Name')
category = tables.Column(verbose_name='Category')
record_type = tables.Column(verbose_name='Type') record_type = tables.Column(verbose_name='Type')
priority = tables.Column(verbose_name='Priority') priority = tables.Column(verbose_name='Priority')
zone = tables.LinkColumn('dns:zone', args=[Accessor('zone.pk')], verbose_name='Zone') zone = tables.LinkColumn('dns:zone', args=[Accessor('zone.pk')], verbose_name='Zone')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model=Record 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]: if form.cleaned_data[field]:
fields_to_update[field] = 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): class ZoneBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@ -146,7 +149,10 @@ class RecordBulkEditView(PermissionRequiredMixin, BulkEditView):
if form.cleaned_data[field]: if form.cleaned_data[field]:
fields_to_update[field] = 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): class RecordBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dns.delete_record' permission_required = 'dns.delete_record'

View File

@ -56,15 +56,6 @@
<td>Name</td> <td>Name</td>
<td>{{ record.name }}</td> <td>{{ record.name }}</td>
</tr> </tr>
<tr>
<td>Category</td>
<td>
{% if record.category %}
{{ record.category }}
{% else %}
-
{% endif %}
</td>
<tr> <tr>
<td>Type</td> <td>Type</td>
<td>{{ record.record_type }}</td> <td>{{ record.record_type }}</td>
@ -130,7 +121,7 @@
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
<tr> <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> </tr>
</table> </table>
</div> </div>

View File

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

View File

@ -38,11 +38,6 @@
<td>Host name, @ for origin</td> <td>Host name, @ for origin</td>
<td>www</td> <td>www</td>
</tr> </tr>
<tr>
<td>Category</td>
<td>Category (e.g. SLA, Server or Customer ; optional)</td>
<td>Server</td>
</tr>
<tr> <tr>
<td>Type</td> <td>Type</td>
<td>Record type</td> <td>Record type</td>
@ -71,7 +66,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <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>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -162,7 +162,7 @@
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
<tr> <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> </tr>
</table> </table>
</div> </div>

View File

@ -48,11 +48,6 @@
<td>The responsible party for the domain</td> <td>The responsible party for the domain</td>
<td>ns.foo.net. noc.foo.net.</td> <td>ns.foo.net. noc.foo.net.</td>
</tr> </tr>
<tr>
<td>SOA Serial</td>
<td>Serial string in SOA record</td>
<td>2016070401</td>
</tr>
<tr> <tr>
<td>SOA Refresh</td> <td>SOA Refresh</td>
<td>Refresh time, in seconds</td> <td>Refresh time, in seconds</td>
@ -81,7 +76,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <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>
</div> </div>
{% endblock %} {% endblock %}