Fixes #166: Full DNS support

This commit is contained in:
rdujardin 2016-07-20 15:24:42 +02:00
parent c643e3a74f
commit 5ea721a6aa
45 changed files with 1369 additions and 5 deletions

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

@ -0,0 +1,19 @@
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.
As zones are readable through the REST API, it is possible to write some external script which automatically generates zone files for a DNS server,
this feature is not directly provided by NetBox though.
---
# 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 hostname if the record is of CNAME type.
Records must be linked to an existing zone, and hold either an existing IP address link or a text value.
Reverse DNS is not supported by Record objects, but by the "Host Name" field in IP addresses.

View File

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

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

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

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

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

View File

View File

@ -0,0 +1,38 @@
from rest_framework import serializers
from ipam.api.serializers import IPAddressNestedSerializer
from dns.models import Zone, Record
#
# Zones
#
class ZoneSerializer(serializers.ModelSerializer):
class Meta:
model=Zone
fields = ['id', 'name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']
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']
class RecordNestedSerializer(RecordSerializer):
class Meta(RecordSerializer.Meta):
fields = ['id', 'name', 'record_type', 'zone']

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

@ -0,0 +1,15 @@
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'),
]

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

@ -0,0 +1,44 @@
from rest_framework import generics
from ipam.models import IPAddress
from dns.models import Zone, Record
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

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

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

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

@ -0,0 +1,36 @@
import django_filters
from ipam.models import IPAddress
from .models import (
Zone,
Record,
)
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)',
)
name = django_filters.CharFilter(
name='name',
lookup_type='icontains',
label='Name',
)
class Meta:
model=Record
field = ['name','record_type','value']

View File

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

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

@ -0,0 +1,109 @@
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,
)
#
# Zones
#
class ZoneForm(forms.ModelForm, BootstrapMixin):
class Meta:
model=Zone
fields = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']
labels = {
'soa_name': 'SOA Name',
'soa_contact': 'SOA Contact',
'soa_serial': 'SOA Serial',
'soa_refresh': 'SOA Refresh',
'soa_retry': 'SOA Retry',
'soa_expire': 'SOA Expire',
'soa_minimum': 'SOA Minimum',
}
class ZoneFromCSVForm(forms.ModelForm):
class Meta:
model=Zone
fields = ['name', 'ttl', 'soa_name', 'soa_contact', 'soa_serial', 'soa_refresh', 'soa_retry', 'soa_expire', 'soa_minimum']
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')
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):
class Meta:
model=Record
fields = ['name', 'record_type', 'priority', 'zone', 'address', 'value']
labels = {
'record_type': 'Type',
}
class RecordFromCSVForm(forms.ModelForm):
zone = forms.ModelChoiceField(queryset=Zone.objects.all(), to_field_name='name', error_messages={'invalid_choice': 'Zone not found.'})
address = forms.ModelChoiceField(queryset=IPAddress.objects.all(), to_field_name='address', error_messages={'invalid_choice': 'IP Address not found.'}, required=False)
class Meta:
model=Record
fields = ['zone', 'name', 'record_type', 'priority', 'address', 'value']
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 = forms.ModelChoiceField(queryset=IPAddress.objects.all(), 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_name_choices():
#name_choices =
class RecordFilterForm(forms.Form, BootstrapMixin):
zone__name = forms.MultipleChoiceField(required=False, choices=record_zone_choices, label='Zone',
widget=forms.SelectMultiple(attrs={'size': 8}))
#name = forms.MultipleChoiceField(required=False, choices=record_name_choices, label='Name', widget=forms.SelectMultiple(attrs={'size': 8}))
record_type = forms.CharField(max_length=100, required=False, label='Type')

View File

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

View File

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

View File

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

@ -0,0 +1,83 @@
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
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=100)
soa_refresh=models.PositiveIntegerField()
soa_retry=models.PositiveIntegerField()
soa_expire=models.PositiveIntegerField()
soa_minimum=models.PositiveIntegerField()
class Meta:
ordering = ['name']
def __unicode__(self):
return self.name
def get_absolute_url(self):
return reverse('dns:zone', args=[self.pk])
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),
])
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.SET_NULL, blank=True, null=True)
value=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:record', args=[self.pk])
def clean(self):
if not self.address and not self.value:
raise ValidationError("DNS records must have either an IP address or a text value")
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 '',
str(self.value) if self.value else '',
])
#def to_json(self):
# return JSON

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

@ -0,0 +1,52 @@
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')

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

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

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

@ -0,0 +1,27 @@
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'),
]

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

@ -0,0 +1,140 @@
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 ipam.models import IPAddress
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
#
# 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)
return render(request, 'dns/zone.html', {
'zone': zone,
'records': records,
'record_count': record_count,
})
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]
return self.cls.objects.filter(pk__in=pk_list).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)
return render(request, 'dns/record.html', {
'record': record,
})
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]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class RecordBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dns.delete_record'
cls = Record
form = forms.RecordBulkEditForm
default_redirect_url = 'dns:record_list'

View File

@ -142,7 +142,7 @@ class IPAddressSerializer(serializers.ModelSerializer):
class Meta:
model = IPAddress
fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
fields = ['id', 'family', 'address', 'vrf', 'hostname', 'interface', 'description', 'nat_inside', 'nat_outside']
class IPAddressNestedSerializer(IPAddressSerializer):

View File

@ -70,6 +70,7 @@
"family": 4,
"address": "10.0.255.1/32",
"vrf": null,
"hostname": "foo.net",
"interface": 3,
"nat_inside": null,
"description": ""
@ -84,6 +85,7 @@
"family": 4,
"address": "169.254.254.1/31",
"vrf": null,
"hostname": "",
"interface": 4,
"nat_inside": null,
"description": ""
@ -98,6 +100,7 @@
"family": 4,
"address": "10.0.255.2/32",
"vrf": null,
"hostname": "",
"interface": 185,
"nat_inside": null,
"description": ""
@ -112,6 +115,7 @@
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"hostname": "",
"interface": 213,
"nat_inside": null,
"description": ""
@ -126,6 +130,7 @@
"family": 4,
"address": "10.0.254.1/24",
"vrf": null,
"hostname": "",
"interface": 12,
"nat_inside": null,
"description": ""
@ -140,6 +145,7 @@
"family": 4,
"address": "10.15.21.1/31",
"vrf": null,
"hostname": "",
"interface": 218,
"nat_inside": null,
"description": ""
@ -154,6 +160,7 @@
"family": 4,
"address": "10.15.21.2/31",
"vrf": null,
"hostname": "",
"interface": 9,
"nat_inside": null,
"description": ""
@ -168,6 +175,7 @@
"family": 4,
"address": "10.15.22.1/31",
"vrf": null,
"hostname": "",
"interface": 8,
"nat_inside": null,
"description": ""
@ -182,6 +190,7 @@
"family": 4,
"address": "10.15.20.1/31",
"vrf": null,
"hostname": "",
"interface": 7,
"nat_inside": null,
"description": ""
@ -196,6 +205,7 @@
"family": 4,
"address": "10.16.20.1/31",
"vrf": null,
"hostname": "",
"interface": 216,
"nat_inside": null,
"description": ""
@ -210,6 +220,7 @@
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"hostname": "",
"interface": 206,
"nat_inside": null,
"description": ""
@ -224,6 +235,7 @@
"family": 4,
"address": "10.16.22.1/31",
"vrf": null,
"hostname": "",
"interface": 217,
"nat_inside": null,
"description": ""
@ -238,6 +250,7 @@
"family": 4,
"address": "10.16.22.2/31",
"vrf": null,
"hostname": "",
"interface": 205,
"nat_inside": null,
"description": ""
@ -252,6 +265,7 @@
"family": 4,
"address": "10.16.20.2/31",
"vrf": null,
"hostname": "",
"interface": 211,
"nat_inside": null,
"description": ""
@ -266,6 +280,7 @@
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"hostname": "",
"interface": 212,
"nat_inside": null,
"description": ""
@ -280,6 +295,7 @@
"family": 4,
"address": "10.0.254.2/32",
"vrf": null,
"hostname": "",
"interface": 188,
"nat_inside": null,
"description": ""
@ -294,6 +310,7 @@
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"hostname": "",
"interface": 200,
"nat_inside": null,
"description": ""
@ -308,6 +325,7 @@
"family": 4,
"address": "169.254.1.2/31",
"vrf": null,
"hostname": "",
"interface": 194,
"nat_inside": null,
"description": ""

View File

@ -311,10 +311,11 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
fields = ['address', 'vrf', 'hostname', 'nat_device', 'nat_inside', 'description']
help_texts = {
'address': "IPv4 or IPv6 address and mask",
'vrf': "VRF (if applicable)",
'hostname': "Reverse DNS host name",
}
def __init__(self, *args, **kwargs):
@ -367,7 +368,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'hostname', 'device', 'interface_name', 'is_primary', 'description']
def clean(self):
@ -414,6 +415,7 @@ class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
help_text="Select the VRF to assign, or check below to remove VRF assignment")
hostname = forms.CharField(max_length=100, required=False)
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
description = forms.CharField(max_length=50, required=False)

View File

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

View File

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

View File

@ -304,6 +304,7 @@ class IPAddress(CreatedUpdatedModel):
address = IPAddressField()
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
hostname = models.CharField(max_length=100, blank=True, verbose_name='Host Name')
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@ -354,6 +355,7 @@ class IPAddress(CreatedUpdatedModel):
return ','.join([
str(self.address),
self.vrf.rd if self.vrf else '',
self.hostname if self.hostname else '',
self.device.identifier if self.device else '',
self.interface.name if self.interface else '',
'True' if is_primary else '',

View File

@ -160,6 +160,7 @@ class IPAddressTable(BaseTable):
pk = ToggleColumn()
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
hostname = tables.Column(verbose_name='Host Name')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
verbose_name='Device')
interface = tables.Column(orderable=False, verbose_name='Interface')
@ -167,7 +168,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
fields = ('pk', 'address', 'vrf', 'hostname', 'device', 'interface', 'description')
class IPAddressBriefTable(BaseTable):

View File

@ -6,6 +6,8 @@ from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from dcim.models import Device
from dns.models import Zone, Record
from dns.tables import RecordBriefTable
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@ -409,11 +411,17 @@ def ipaddress(request, pk):
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
related_ips_table = tables.IPAddressBriefTable(related_ips)
# Related DNS records
dns_records = Record.objects.filter(address=ipaddress)
dns_records_table = RecordBriefTable(dns_records)
return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress,
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
'related_ips_table': related_ips_table,
'dns_records_table': dns_records_table,
})

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,105 @@
{% 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>Created</td>
<td>{{ record.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ record.last_updated }}</td>
</tr>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% 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>
</tr>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,67 @@
{% 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 zone</td>
<td>foo.net</td>
</tr>
<tr>
<td>Name</td>
<td>Name of record, @ for origin</td>
<td>www</td>
</tr>
<tr>
<td>Type</td>
<td>Type of record</td>
<td>AAAA</td>
</tr>
<tr>
<td>Priority</td>
<td>Priority level of record (optional)</td>
<td>30</td>
</tr>
<tr>
<td>Address</td>
<td>IP address value</td>
<td>192.168.1.110/16</td>
</tr>
<tr>
<td>Value</td>
<td>Text value, for CNAME records for instance</td>
<td>foo.net</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>foo.net,www,AAAA,,192.168.1.110/16,</pre>
</div>
</div>
{% endblock %}

View File

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

View File

@ -0,0 +1,142 @@
{% 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>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">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Records</strong>
</div>
{% if records %}
<table class="table table-hover panel-body">
{% for r in records %}
<tr>
<td>
<a href="{% url 'dns:record' pk=r.pk %}">{{ r }}</a>
</td>
<td>{{ r.record_type }}</td>
<td>
{% if r.priority %}
{{ r.priority }}
{% else %}
-
{% endif %}
</td>
<td>
{% if r.address %}
<a href="{% url 'ipam:ipaddress' pk=r.address.pk %}">{{ r.address }}</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if r.value %}
{{ r.value }}
{% else %}
-
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% 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>
</tr>
{% endfor %}
{% endblock %}

View File

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

View File

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

View File

@ -122,6 +122,23 @@
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>DNS</strong>
</div>
<div class="list-group">
<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 class="panel panel-default">
<div class="panel-heading">
<strong>Circuits</strong>

View File

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

View File

@ -8,6 +8,7 @@
<tr>
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
<td>{{ ipaddress.vrf }}</td>
<td>{{ ipaddress.hostname }}</td>
<td>{{ ipaddress.interface.device }}</td>
<td>{{ ipaddress.interface }}</td>
<td>{{ ipaddress.description }}</td>

View File

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

View File

@ -38,6 +38,11 @@
<td>VRF route distinguisher (optional)</td>
<td>65000:123</td>
</tr>
<tr>
<td>Host Name</td>
<td>Reverse DNS host name</td>
<td>foo.com</td>
</tr>
<tr>
<td>Device</td>
<td>Device name (optional)</td>
@ -61,7 +66,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>192.0.2.42/24,65000:123,switch12,ge-0/0/31,True,Management IP</pre>
<pre>192.0.2.42/24,65000:123,foo.com,switch12,ge-0/0/31,True,Management IP</pre>
</div>
</div>
{% endblock %}