From 117bf1a118defead7f6843b4fa777d02c5c37e74 Mon Sep 17 00:00:00 2001 From: rdujardin Date: Fri, 22 Jul 2016 12:28:19 +0200 Subject: [PATCH] Remove category field in record & add auto-updating serial in zone --- docs/data-model/dns.md | 8 +++- netbox/dns/filters.py | 9 +--- netbox/dns/forms.py | 33 ++++++--------- .../dns/migrations/0004_auto_20160722_0820.py | 29 +++++++++++++ netbox/dns/models.py | 42 ++++++++++++++++--- netbox/dns/tables.py | 6 +-- netbox/dns/views.py | 10 ++++- netbox/templates/dns/record.html | 11 +---- netbox/templates/dns/record_bulk_edit.html | 1 - netbox/templates/dns/record_import.html | 7 +--- netbox/templates/dns/zone.html | 2 +- netbox/templates/dns/zone_import.html | 7 +--- 12 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 netbox/dns/migrations/0004_auto_20160722_0820.py diff --git a/docs/data-model/dns.md b/docs/data-model/dns.md index 3951e9c41..5ac5218f0 100644 --- a/docs/data-model/dns.md +++ b/docs/data-model/dns.md @@ -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. --- diff --git a/netbox/dns/filters.py b/netbox/dns/filters.py index 0f54af96e..480aa956a 100644 --- a/netbox/dns/filters.py +++ b/netbox/dns/filters.py @@ -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: diff --git a/netbox/dns/forms.py b/netbox/dns/forms.py index 0115fe4c9..72f8dc7d8 100644 --- a/netbox/dns/forms.py +++ b/netbox/dns/forms.py @@ -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})) diff --git a/netbox/dns/migrations/0004_auto_20160722_0820.py b/netbox/dns/migrations/0004_auto_20160722_0820.py new file mode 100644 index 000000000..5087660aa --- /dev/null +++ b/netbox/dns/migrations/0004_auto_20160722_0820.py @@ -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'), + ), + ] diff --git a/netbox/dns/models.py b/netbox/dns/models.py index a5857cabc..4efb3ba90 100644 --- a/netbox/dns/models.py +++ b/netbox/dns/models.py @@ -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 '', diff --git a/netbox/dns/tables.py b/netbox/dns/tables.py index 2b48706bc..8d0177584 100644 --- a/netbox/dns/tables.py +++ b/netbox/dns/tables.py @@ -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') \ No newline at end of file diff --git a/netbox/dns/views.py b/netbox/dns/views.py index c2c55c6c6..2dd35b6ae 100644 --- a/netbox/dns/views.py +++ b/netbox/dns/views.py @@ -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' diff --git a/netbox/templates/dns/record.html b/netbox/templates/dns/record.html index 00055d89c..8e7cbdf42 100644 --- a/netbox/templates/dns/record.html +++ b/netbox/templates/dns/record.html @@ -56,15 +56,6 @@ Name {{ record.name }} - - Category - - {% if record.category %} - {{ record.category }} - {% else %} - - - {% endif %} - Type {{ record.record_type }} @@ -130,7 +121,7 @@ - +
{{ bind_export }}

{{ bind_export }}

diff --git a/netbox/templates/dns/record_bulk_edit.html b/netbox/templates/dns/record_bulk_edit.html index e5a389a03..5e4586bfa 100644 --- a/netbox/templates/dns/record_bulk_edit.html +++ b/netbox/templates/dns/record_bulk_edit.html @@ -7,7 +7,6 @@ {% for record in selected_objects %} {{ record.name }} - {{ record.category }} {{ record.record_type }} {{ record.priority }} {{ record.address }} diff --git a/netbox/templates/dns/record_import.html b/netbox/templates/dns/record_import.html index 28ca8bc6d..ca428a2e5 100644 --- a/netbox/templates/dns/record_import.html +++ b/netbox/templates/dns/record_import.html @@ -38,11 +38,6 @@ Host name, @ for origin www - - Category - Category (e.g. SLA, Server or Customer ; optional) - Server - Type Record type @@ -71,7 +66,7 @@

Example

-
foo.net,www,Server,AAAA,,192.168.1.110/16,,Backend API server
+
foo.net,www,AAAA,,192.168.1.110/16,,Backend API server
{% endblock %} diff --git a/netbox/templates/dns/zone.html b/netbox/templates/dns/zone.html index 4b76d5ea8..317278c86 100644 --- a/netbox/templates/dns/zone.html +++ b/netbox/templates/dns/zone.html @@ -162,7 +162,7 @@ - +
{{ bind_export }}

{{ bind_export }}

diff --git a/netbox/templates/dns/zone_import.html b/netbox/templates/dns/zone_import.html index 62dd3e8d0..e24a6ebce 100644 --- a/netbox/templates/dns/zone_import.html +++ b/netbox/templates/dns/zone_import.html @@ -48,11 +48,6 @@ The responsible party for the domain ns.foo.net. noc.foo.net. - - SOA Serial - Serial string in SOA record - 2016070401 - SOA Refresh Refresh time, in seconds @@ -81,7 +76,7 @@

Example

-
foo.net,10800,@,ns.foo.net. noc.foo.net.,2016070401,3600,3600,604800,1800,Mail servers zone
+
foo.net,10800,@,ns.foo.net. noc.foo.net.,3600,3600,604800,1800,Mail servers zone
{% endblock %}