mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 19:08:38 -06:00
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:
commit
7dafbae4bd
41
docs/data-model/dns.md
Normal file
41
docs/data-model/dns.md
Normal 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.
|
||||||
|
|
@ -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:
|
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
|
* **IP address management (IPAM)** - IP networks and addresses, VRFs, and VLANs
|
||||||
|
* **DNS management** - DNS zones and records
|
||||||
* **Equipment racks** - Organized by group and site
|
* **Equipment racks** - Organized by group and site
|
||||||
* **Devices** - Types of devices and where they are installed
|
* **Devices** - Types of devices and where they are installed
|
||||||
* **Connections** - Network, console, and power connections among devices
|
* **Connections** - Network, console, and power connections among devices
|
||||||
|
1
netbox/dns/__init__.py
Normal file
1
netbox/dns/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'dns.apps.DNSConfig'
|
17
netbox/dns/admin.py
Normal file
17
netbox/dns/admin.py
Normal 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']
|
0
netbox/dns/api/__init__.py
Normal file
0
netbox/dns/api/__init__.py
Normal file
38
netbox/dns/api/serializers.py
Normal file
38
netbox/dns/api/serializers.py
Normal 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
19
netbox/dns/api/urls.py
Normal 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
69
netbox/dns/api/views.py
Normal 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
6
netbox/dns/apps.py
Normal 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
51
netbox/dns/filters.py
Normal 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))
|
37
netbox/dns/fixtures/dns.json
Normal file
37
netbox/dns/fixtures/dns.json
Normal 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
41
netbox/dns/formfields.py
Normal 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
147
netbox/dns/forms.py
Normal 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}))
|
||||||
|
|
59
netbox/dns/migrations/0001_initial.py
Normal file
59
netbox/dns/migrations/0001_initial.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
20
netbox/dns/migrations/0002_auto_20160719_1058.py
Normal file
20
netbox/dns/migrations/0002_auto_20160719_1058.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
34
netbox/dns/migrations/0003_auto_20160721_1059.py
Normal file
34
netbox/dns/migrations/0003_auto_20160721_1059.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
29
netbox/dns/migrations/0004_auto_20160722_0820.py
Normal file
29
netbox/dns/migrations/0004_auto_20160722_0820.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
25
netbox/dns/migrations/0005_auto_20160728_0854.py
Normal file
25
netbox/dns/migrations/0005_auto_20160728_0854.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
0
netbox/dns/migrations/__init__.py
Normal file
0
netbox/dns/migrations/__init__.py
Normal file
214
netbox/dns/models.py
Normal file
214
netbox/dns/models.py
Normal 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
63
netbox/dns/tables.py
Normal 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
3
netbox/dns/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
31
netbox/dns/urls.py
Normal file
31
netbox/dns/urls.py
Normal 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
201
netbox/dns/views.py
Normal 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')
|
||||||
|
|
@ -136,7 +136,7 @@ class PrefixSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
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):
|
class PrefixNestedSerializer(PrefixSerializer):
|
||||||
@ -156,7 +156,7 @@ class IPAddressSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
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):
|
class IPAddressNestedSerializer(IPAddressSerializer):
|
||||||
|
@ -42,7 +42,15 @@
|
|||||||
"vlan": null,
|
"vlan": null,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"role": 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,
|
"vlan": null,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"role": 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,
|
"family": 4,
|
||||||
"address": "10.0.255.1/32",
|
"address": "10.0.255.1/32",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "www.foo.net",
|
||||||
"interface": 3,
|
"interface": 3,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -84,6 +101,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "169.254.254.1/31",
|
"address": "169.254.254.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 4,
|
"interface": 4,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -98,6 +116,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.0.255.2/32",
|
"address": "10.0.255.2/32",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 185,
|
"interface": 185,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -112,6 +131,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "169.254.1.1/31",
|
"address": "169.254.1.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 213,
|
"interface": 213,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -126,6 +146,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.0.254.1/24",
|
"address": "10.0.254.1/24",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 12,
|
"interface": 12,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -140,6 +161,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.15.21.1/31",
|
"address": "10.15.21.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 218,
|
"interface": 218,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -154,6 +176,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.15.21.2/31",
|
"address": "10.15.21.2/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 9,
|
"interface": 9,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -168,6 +191,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.15.22.1/31",
|
"address": "10.15.22.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 8,
|
"interface": 8,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -182,6 +206,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.15.20.1/31",
|
"address": "10.15.20.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 7,
|
"interface": 7,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -196,6 +221,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.16.20.1/31",
|
"address": "10.16.20.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 216,
|
"interface": 216,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -210,6 +236,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.15.22.2/31",
|
"address": "10.15.22.2/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 206,
|
"interface": 206,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -224,6 +251,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.16.22.1/31",
|
"address": "10.16.22.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 217,
|
"interface": 217,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -238,6 +266,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.16.22.2/31",
|
"address": "10.16.22.2/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 205,
|
"interface": 205,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -252,6 +281,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.16.20.2/31",
|
"address": "10.16.20.2/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 211,
|
"interface": 211,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -266,6 +296,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.15.22.2/31",
|
"address": "10.15.22.2/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 212,
|
"interface": 212,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -280,6 +311,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "10.0.254.2/32",
|
"address": "10.0.254.2/32",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 188,
|
"interface": 188,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -294,6 +326,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "169.254.1.1/31",
|
"address": "169.254.1.1/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 200,
|
"interface": 200,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -308,6 +341,7 @@
|
|||||||
"family": 4,
|
"family": 4,
|
||||||
"address": "169.254.1.2/31",
|
"address": "169.254.1.2/31",
|
||||||
"vrf": null,
|
"vrf": null,
|
||||||
|
"ptr": "",
|
||||||
"interface": 194,
|
"interface": 194,
|
||||||
"nat_inside": null,
|
"nat_inside": null,
|
||||||
"description": ""
|
"description": ""
|
||||||
|
@ -158,7 +158,16 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
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 = {
|
help_texts = {
|
||||||
'prefix': "IPv4 or IPv6 network",
|
'prefix': "IPv4 or IPv6 network",
|
||||||
'vrf': "VRF (if applicable)",
|
'vrf': "VRF (if applicable)",
|
||||||
@ -166,6 +175,13 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
|
|||||||
'vlan': "The VLAN to which this prefix is assigned (if applicable)",
|
'vlan': "The VLAN to which this prefix is assigned (if applicable)",
|
||||||
'status': "Operational status of this prefix",
|
'status': "Operational status of this prefix",
|
||||||
'role': "The primary function 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):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -212,7 +228,7 @@ class PrefixFromCSVForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
|
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):
|
def clean(self):
|
||||||
|
|
||||||
@ -264,6 +280,14 @@ class PrefixBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=100, 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():
|
def prefix_vrf_choices():
|
||||||
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
|
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
|
||||||
@ -326,10 +350,11 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
|
fields = ['address', 'vrf', 'tenant', 'ptr', 'nat_device', 'nat_inside', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'address': "IPv4 or IPv6 address and mask",
|
'address': "IPv4 or IPv6 address and mask",
|
||||||
'vrf': "VRF (if applicable)",
|
'vrf': "VRF (if applicable)",
|
||||||
|
'ptr': "Reverse DNS name",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -384,7 +409,8 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
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):
|
def clean(self):
|
||||||
|
|
||||||
@ -430,10 +456,12 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
|
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
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')
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def ipaddress_family_choices():
|
def ipaddress_family_choices():
|
||||||
return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]
|
return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]
|
||||||
|
|
||||||
|
20
netbox/ipam/migrations/0005_ipaddress_hostname.py
Normal file
20
netbox/ipam/migrations/0005_ipaddress_hostname.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
netbox/ipam/migrations/0006_auto_20160720_0941.py
Normal file
20
netbox/ipam/migrations/0006_auto_20160720_0941.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
55
netbox/ipam/migrations/0007_auto_20160722_0958.py
Normal file
55
netbox/ipam/migrations/0007_auto_20160722_0958.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
24
netbox/ipam/migrations/0008_auto_20160727_1307.py
Normal file
24
netbox/ipam/migrations/0008_auto_20160727_1307.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
25
netbox/ipam/migrations/0009_auto_20160728_0854.py
Normal file
25
netbox/ipam/migrations/0009_auto_20160728_0854.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -9,9 +9,13 @@ from django.db import models
|
|||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
import dns.models
|
||||||
|
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
|
|
||||||
|
import time, ipaddress
|
||||||
|
import netaddr
|
||||||
|
|
||||||
|
|
||||||
AF_CHOICES = (
|
AF_CHOICES = (
|
||||||
(4, 'IPv4'),
|
(4, 'IPv4'),
|
||||||
@ -238,10 +242,24 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
verbose_name='VLAN')
|
verbose_name='VLAN')
|
||||||
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
|
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)
|
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()
|
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:
|
class Meta:
|
||||||
ordering = ['family', 'prefix']
|
ordering = ['family', 'prefix']
|
||||||
verbose_name_plural = 'prefixes'
|
verbose_name_plural = 'prefixes'
|
||||||
@ -262,6 +280,7 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
"instead.")
|
"instead.")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
self.bind_changed = True
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
# Clear host bits from prefix
|
# Clear host bits from prefix
|
||||||
self.prefix = self.prefix.cidr
|
self.prefix = self.prefix.cidr
|
||||||
@ -269,6 +288,32 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
self.family = self.prefix.version
|
self.family = self.prefix.version
|
||||||
super(Prefix, self).save(*args, **kwargs)
|
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):
|
def to_csv(self):
|
||||||
return ','.join([
|
return ','.join([
|
||||||
str(self.prefix),
|
str(self.prefix),
|
||||||
@ -277,6 +322,14 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
self.role.name if self.role else '',
|
self.role.name if self.role else '',
|
||||||
self.description,
|
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
|
@property
|
||||||
@ -293,6 +346,121 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
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):
|
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,
|
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VRF')
|
verbose_name='VRF')
|
||||||
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
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,
|
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=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()))
|
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
self.update_dns()
|
||||||
if self.address:
|
if self.address:
|
||||||
# Infer address family from IPAddress object
|
# Infer address family from IPAddress object
|
||||||
self.family = self.address.version
|
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)
|
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):
|
def to_csv(self):
|
||||||
|
|
||||||
# Determine if this IP is primary for a Device
|
# Determine if this IP is primary for a Device
|
||||||
@ -360,6 +557,7 @@ class IPAddress(CreatedUpdatedModel):
|
|||||||
return ','.join([
|
return ','.join([
|
||||||
str(self.address),
|
str(self.address),
|
||||||
self.vrf.rd if self.vrf else '',
|
self.vrf.rd if self.vrf else '',
|
||||||
|
self.ptr if self.ptr else '',
|
||||||
self.device.identifier if self.device else '',
|
self.device.identifier if self.device else '',
|
||||||
self.interface.name if self.interface else '',
|
self.interface.name if self.interface else '',
|
||||||
'True' if is_primary else '',
|
'True' if is_primary else '',
|
||||||
|
@ -172,6 +172,7 @@ class IPAddressTable(BaseTable):
|
|||||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
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')
|
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
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,
|
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||||
verbose_name='Device')
|
verbose_name='Device')
|
||||||
interface = tables.Column(orderable=False, verbose_name='Interface')
|
interface = tables.Column(orderable=False, verbose_name='Interface')
|
||||||
@ -179,7 +180,7 @@ class IPAddressTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
fields = ('pk', 'address', 'vrf', 'tenant', 'ptr', 'device', 'interface', 'description')
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBriefTable(BaseTable):
|
class IPAddressBriefTable(BaseTable):
|
||||||
|
@ -6,6 +6,8 @@ from django.db.models import Count, Q
|
|||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
|
from dns.models import Zone, Record
|
||||||
|
from dns.tables import RecordBriefTable
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
@ -276,9 +278,10 @@ def prefix(request, pk):
|
|||||||
except Aggregate.DoesNotExist:
|
except Aggregate.DoesNotExist:
|
||||||
aggregate = None
|
aggregate = None
|
||||||
|
|
||||||
|
child_ip = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))
|
||||||
|
|
||||||
# Count child IP addresses
|
# Count child IP addresses
|
||||||
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
ipaddress_count = child_ip.count()
|
||||||
.count()
|
|
||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
|
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
|
fields_to_update[field] = None
|
||||||
elif form.cleaned_data[field]:
|
elif form.cleaned_data[field]:
|
||||||
fields_to_update[field] = 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]:
|
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)
|
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):
|
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
@ -419,11 +425,16 @@ def ipaddress(request, pk):
|
|||||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
||||||
related_ips_table = tables.IPAddressBriefTable(related_ips)
|
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', {
|
return render(request, 'ipam/ipaddress.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
'parent_prefixes_table': parent_prefixes_table,
|
'parent_prefixes_table': parent_prefixes_table,
|
||||||
'duplicate_ips_table': duplicate_ips_table,
|
'duplicate_ips_table': duplicate_ips_table,
|
||||||
'related_ips_table': related_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
|
fields_to_update[field] = None
|
||||||
elif form.cleaned_data[field]:
|
elif form.cleaned_data[field]:
|
||||||
fields_to_update[field] = 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]:
|
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)
|
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):
|
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
@ -106,6 +106,7 @@ INSTALLED_APPS = (
|
|||||||
'circuits',
|
'circuits',
|
||||||
'dcim',
|
'dcim',
|
||||||
'ipam',
|
'ipam',
|
||||||
|
'dns',
|
||||||
'extras',
|
'extras',
|
||||||
'secrets',
|
'secrets',
|
||||||
'tenancy',
|
'tenancy',
|
||||||
|
@ -21,6 +21,7 @@ urlpatterns = [
|
|||||||
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
|
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
|
||||||
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
||||||
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
|
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'^secrets/', include('secrets.urls', namespace='secrets')),
|
||||||
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
|
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
|
||||||
url(r'^profile/', include('users.urls', namespace='users')),
|
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/circuits/', include('circuits.api.urls', namespace='circuits-api')),
|
||||||
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-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/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/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
||||||
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
||||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
||||||
|
@ -6,6 +6,7 @@ from circuits.models import Provider, Circuit
|
|||||||
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
||||||
from extras.models import UserAction
|
from extras.models import UserAction
|
||||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
||||||
|
from dns.models import Zone, Record
|
||||||
from secrets.models import Secret
|
from secrets.models import Secret
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
|
||||||
@ -32,6 +33,10 @@ def home(request):
|
|||||||
'ipaddress_count': IPAddress.objects.count(),
|
'ipaddress_count': IPAddress.objects.count(),
|
||||||
'vlan_count': VLAN.objects.count(),
|
'vlan_count': VLAN.objects.count(),
|
||||||
|
|
||||||
|
# DNS
|
||||||
|
'zone_count': Zone.objects.count(),
|
||||||
|
'record_count': Record.objects.count(),
|
||||||
|
|
||||||
# Circuits
|
# Circuits
|
||||||
'provider_count': Provider.objects.count(),
|
'provider_count': Provider.objects.count(),
|
||||||
'circuit_count': Circuit.objects.count(),
|
'circuit_count': Circuit.objects.count(),
|
||||||
|
@ -9,7 +9,7 @@ html, body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
padding-top: 70px;
|
padding-top: 60px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: auto;
|
width: auto;
|
||||||
@ -19,7 +19,7 @@ body {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
|
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
|
||||||
padding-bottom: 30px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
.footer, .push {
|
.footer, .push {
|
||||||
height: 60px; /* .push must be the same height as .footer */
|
height: 60px; /* .push must be the same height as .footer */
|
||||||
|
@ -165,6 +165,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</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 %}">
|
<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>
|
<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">
|
<ul class="dropdown-menu">
|
||||||
|
64
netbox/templates/dns/bind_export.html
Normal file
64
netbox/templates/dns/bind_export.html
Normal 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 %}
|
147
netbox/templates/dns/record.html
Normal file
147
netbox/templates/dns/record.html
Normal 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 %}
|
18
netbox/templates/dns/record_bulk_edit.html
Normal file
18
netbox/templates/dns/record_bulk_edit.html
Normal 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 %}
|
72
netbox/templates/dns/record_import.html
Normal file
72
netbox/templates/dns/record_import.html
Normal 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 %}
|
47
netbox/templates/dns/record_list.html
Normal file
47
netbox/templates/dns/record_list.html
Normal 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 %}
|
116
netbox/templates/dns/zone.html
Normal file
116
netbox/templates/dns/zone.html
Normal 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 %}
|
20
netbox/templates/dns/zone_bulk_edit.html
Normal file
20
netbox/templates/dns/zone_bulk_edit.html
Normal 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 %}
|
82
netbox/templates/dns/zone_import.html
Normal file
82
netbox/templates/dns/zone_import.html
Normal 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 %}
|
46
netbox/templates/dns/zone_list.html
Normal file
46
netbox/templates/dns/zone_list.html
Normal 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 %}
|
@ -127,18 +127,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Circuits</strong>
|
<strong>DNS</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<span class="badge pull-right">{{ stats.provider_count }}</span>
|
<h4 class="list-group-item-heading"><a href="{% url 'dns:full_forward' %}">Export Forward</a></h4>
|
||||||
<h4 class="list-group-item-heading"><a href="{% url 'circuits:provider_list' %}">Providers</a></h4>
|
<p class="list-group-item-text text-muted">Export forward zones into Bind format</p>
|
||||||
<p class="list-group-item-text text-muted">Organizations which provide circuit connectivity</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<span class="badge pull-right">{{ stats.circuit_count }}</span>
|
<h4 class="list-group-item-heading"><a href="{% url 'dns:full_reverse' %}">Export Reverse</a></h4>
|
||||||
<h4 class="list-group-item-heading"><a href="{% url 'circuits:circuit_list' %}">Circuits</a></h4>
|
<p class="list-group-item-text text-muted">Export reverse zones into Bind format</p>
|
||||||
<p class="list-group-item-text text-muted">Communication links for Internet transit, peering, and other services</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,6 +76,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>PTR</td>
|
||||||
|
<td>
|
||||||
|
{% if ipaddress.ptr %}
|
||||||
|
<span>{{ ipaddress.ptr }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td>
|
<td>
|
||||||
@ -142,6 +152,9 @@
|
|||||||
{% with heading='Related IP Addresses' %}
|
{% with heading='Related IP Addresses' %}
|
||||||
{% render_table related_ips_table 'panel_table.html' %}
|
{% render_table related_ips_table 'panel_table.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% with heading='Related DNS Records' %}
|
||||||
|
{% render_table dns_records_table 'panel_table.html' %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
|
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
|
||||||
<td>{{ ipaddress.vrf|default:"Global" }}</td>
|
<td>{{ ipaddress.vrf|default:"Global" }}</td>
|
||||||
<td>{{ ipaddress.tenant }}</td>
|
<td>{{ ipaddress.tenant }}</td>
|
||||||
|
<td>{{ ipaddress.ptr }}</td>
|
||||||
<td>{{ ipaddress.interface.device }}</td>
|
<td>{{ ipaddress.interface.device }}</td>
|
||||||
<td>{{ ipaddress.interface }}</td>
|
<td>{{ ipaddress.interface }}</td>
|
||||||
<td>{{ ipaddress.description }}</td>
|
<td>{{ ipaddress.description }}</td>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
{% render_field form.address %}
|
{% render_field form.address %}
|
||||||
{% render_field form.vrf %}
|
{% render_field form.vrf %}
|
||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
|
{% render_field form.ptr %}
|
||||||
{% if obj %}
|
{% if obj %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">Device</label>
|
<label class="col-md-3 control-label">Device</label>
|
||||||
|
@ -43,6 +43,11 @@
|
|||||||
<td>Name of tenant (optional)</td>
|
<td>Name of tenant (optional)</td>
|
||||||
<td>ABC01</td>
|
<td>ABC01</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>PTR</td>
|
||||||
|
<td>Reverse DNS Name</td>
|
||||||
|
<td>foo.com</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device</td>
|
<td>Device</td>
|
||||||
<td>Device name (optional)</td>
|
<td>Device name (optional)</td>
|
||||||
@ -66,7 +71,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -99,6 +99,38 @@
|
|||||||
<td>IP Addresses</td>
|
<td>IP Addresses</td>
|
||||||
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }}</a></td>
|
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }}</a></td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td>Created</td>
|
<td>Created</td>
|
||||||
<td>{{ prefix.created }}</td>
|
<td>{{ prefix.created }}</td>
|
||||||
@ -133,3 +165,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -13,6 +13,14 @@
|
|||||||
<td>{{ prefix.status }}</td>
|
<td>{{ prefix.status }}</td>
|
||||||
<td>{{ prefix.role }}</td>
|
<td>{{ prefix.role }}</td>
|
||||||
<td>{{ prefix.description }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -73,10 +73,45 @@
|
|||||||
<td>Short description (optional)</td>
|
<td>Short description (optional)</td>
|
||||||
<td>7th floor WiFi</td>
|
<td>7th floor WiFi</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user