mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Merge branch 'develop' of https://github.com/Zanthras/netbox into develop
This commit is contained in:
commit
b3e60210b7
@ -9,6 +9,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
||||||
POSTGRES_DB: netbox
|
POSTGRES_DB: netbox
|
||||||
netbox:
|
netbox:
|
||||||
|
build: .
|
||||||
image: digitalocean/netbox
|
image: digitalocean/netbox
|
||||||
links:
|
links:
|
||||||
- postgres
|
- postgres
|
||||||
|
@ -9,7 +9,21 @@ NetBox is an open source web application designed to help manage and document co
|
|||||||
* **Data circuits** - Long-haul communications circuits and providers
|
* **Data circuits** - Long-haul communications circuits and providers
|
||||||
* **Secrets** - Encrypted storage of sensitive credentials
|
* **Secrets** - Encrypted storage of sensitive credentials
|
||||||
|
|
||||||
It was designed with the following tenets foremost in mind.
|
# What NetBox Isn't
|
||||||
|
|
||||||
|
While NetBox strives to cover many areas of network management, the scope of its feature set is necessarily limited. This ensures that development focuses on core functionality and that scope creep is reasonably contained. To that end, it might help to provide some examples of functionality that NetBox **does not** provide:
|
||||||
|
|
||||||
|
* Network monitoring
|
||||||
|
* DNS server
|
||||||
|
* RADIUS server
|
||||||
|
* Configuration management
|
||||||
|
* Facilities management
|
||||||
|
|
||||||
|
That said, NetBox _can_ be used to great effect in populating external tools with the data they need to perform these functions.
|
||||||
|
|
||||||
|
# Design Philosophy
|
||||||
|
|
||||||
|
NetBox was designed with the following tenets foremost in mind.
|
||||||
|
|
||||||
## Replicate the Real World
|
## Replicate the Real World
|
||||||
|
|
||||||
|
@ -9,9 +9,10 @@ NetBox requires following system dependencies:
|
|||||||
* libxslt1-dev
|
* libxslt1-dev
|
||||||
* libffi-dev
|
* libffi-dev
|
||||||
* graphviz
|
* graphviz
|
||||||
|
* libpq-dev
|
||||||
|
|
||||||
```
|
```
|
||||||
# sudo apt-get install -y python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz
|
# sudo apt-get install -y python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
||||||
|
@ -89,7 +89,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
|
|||||||
power_port_count=Count('power_port_templates', distinct=True),
|
power_port_count=Count('power_port_templates', distinct=True),
|
||||||
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
||||||
interface_count=Count('interface_templates', distinct=True),
|
interface_count=Count('interface_templates', distinct=True),
|
||||||
devicebay_count=Count('devicebay_templates', distinct=True),
|
devicebay_count=Count('device_bay_templates', distinct=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
def console_ports(self, instance):
|
def console_ports(self, instance):
|
||||||
@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')
|
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
|
||||||
|
@ -221,12 +221,14 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
platform = PlatformNestedSerializer()
|
platform = PlatformNestedSerializer()
|
||||||
rack = RackNestedSerializer()
|
rack = RackNestedSerializer()
|
||||||
primary_ip = DeviceIPAddressNestedSerializer()
|
primary_ip = DeviceIPAddressNestedSerializer()
|
||||||
|
primary_ip4 = DeviceIPAddressNestedSerializer()
|
||||||
|
primary_ip6 = DeviceIPAddressNestedSerializer()
|
||||||
parent_device = serializers.SerializerMethodField()
|
parent_device = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
||||||
'face', 'parent_device', 'status', 'primary_ip', 'comments']
|
'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
|
||||||
|
|
||||||
def get_parent_device(self, obj):
|
def get_parent_device(self, obj):
|
||||||
try:
|
try:
|
||||||
|
@ -194,7 +194,7 @@ class DeviceListView(generics.ListAPIView):
|
|||||||
List devices (filterable)
|
List devices (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
|
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
|
||||||
.prefetch_related('primary_ip__nat_outside')
|
.prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
|
||||||
serializer_class = serializers.DeviceSerializer
|
serializer_class = serializers.DeviceSerializer
|
||||||
filter_class = filters.DeviceFilter
|
filter_class = filters.DeviceFilter
|
||||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
|
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
|
||||||
|
@ -1919,7 +1919,8 @@
|
|||||||
"position": 1,
|
"position": 1,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": 1,
|
"primary_ip4": 1,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1938,7 +1939,8 @@
|
|||||||
"position": 17,
|
"position": 17,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": 5,
|
"primary_ip4": 5,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1957,7 +1959,8 @@
|
|||||||
"position": 33,
|
"position": 33,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1976,7 +1979,8 @@
|
|||||||
"position": 34,
|
"position": 34,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1995,7 +1999,8 @@
|
|||||||
"position": 34,
|
"position": 34,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2014,7 +2019,8 @@
|
|||||||
"position": 33,
|
"position": 33,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2033,7 +2039,8 @@
|
|||||||
"position": 1,
|
"position": 1,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": 3,
|
"primary_ip4": 3,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2052,7 +2059,8 @@
|
|||||||
"position": 17,
|
"position": 17,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": 19,
|
"primary_ip4": 19,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2071,7 +2079,8 @@
|
|||||||
"position": 42,
|
"position": 42,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2090,7 +2099,8 @@
|
|||||||
"position": null,
|
"position": null,
|
||||||
"face": null,
|
"face": null,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2109,7 +2119,8 @@
|
|||||||
"position": null,
|
"position": null,
|
||||||
"face": null,
|
"face": null,
|
||||||
"status": true,
|
"status": true,
|
||||||
"primary_ip": null,
|
"primary_ip4": null,
|
||||||
|
"primary_ip6": null,
|
||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -349,7 +349,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
||||||
'platform', 'primary_ip', 'comments']
|
'platform', 'primary_ip4', 'primary_ip6', 'comments']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'device_role': "The function this device serves",
|
'device_role': "The function this device serves",
|
||||||
'serial': "Chassis serial number",
|
'serial': "Chassis serial number",
|
||||||
@ -369,20 +369,23 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
self.initial['site'] = self.instance.rack.site
|
self.initial['site'] = self.instance.rack.site
|
||||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||||
|
|
||||||
# Compile list of IPs assigned to this device
|
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||||
primary_ip_choices = []
|
for family in [4, 6]:
|
||||||
interface_ips = IPAddress.objects.filter(interface__device=self.instance)
|
ip_choices = []
|
||||||
primary_ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||||
nat_ips = IPAddress.objects.filter(nat_inside__interface__device=self.instance)\
|
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||||
|
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||||
.select_related('nat_inside__interface')
|
.select_related('nat_inside__interface')
|
||||||
primary_ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||||
self.fields['primary_ip'].choices = [(None, '---------')] + primary_ip_choices
|
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# An object that doesn't exist yet can't have any IPs assigned to it
|
# An object that doesn't exist yet can't have any IPs assigned to it
|
||||||
self.fields['primary_ip'].choices = []
|
self.fields['primary_ip4'].choices = []
|
||||||
self.fields['primary_ip'].widget.attrs['readonly'] = True
|
self.fields['primary_ip4'].widget.attrs['readonly'] = True
|
||||||
|
self.fields['primary_ip6'].choices = []
|
||||||
|
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
# Limit rack choices
|
# Limit rack choices
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
|
27
netbox/dcim/migrations/0006_add_device_primary_ip4_ip6.py
Normal file
27
netbox/dcim/migrations/0006_add_device_primary_ip4_ip6.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-11 18:40
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0001_initial'),
|
||||||
|
('dcim', '0005_auto_20160706_1722'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='primary_ip4',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='primary_ip6',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'),
|
||||||
|
),
|
||||||
|
]
|
41
netbox/dcim/migrations/0007_device_copy_primary_ip.py
Normal file
41
netbox/dcim/migrations/0007_device_copy_primary_ip.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-11 18:40
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def copy_primary_ip(apps, schema_editor):
|
||||||
|
Device = apps.get_model('dcim', 'Device')
|
||||||
|
for d in Device.objects.select_related('primary_ip'):
|
||||||
|
if not d.primary_ip:
|
||||||
|
continue
|
||||||
|
if d.primary_ip.family == 4:
|
||||||
|
d.primary_ip4 = d.primary_ip
|
||||||
|
elif d.primary_ip.family == 6:
|
||||||
|
d.primary_ip6 = d.primary_ip
|
||||||
|
d.save()
|
||||||
|
|
||||||
|
|
||||||
|
def restore_primary_ip(apps, schema_editor):
|
||||||
|
Device = apps.get_model('dcim', 'Device')
|
||||||
|
for d in Device.objects.select_related('primary_ip4', 'primary_ip6'):
|
||||||
|
if d.primary_ip:
|
||||||
|
continue
|
||||||
|
# Prefer IPv6 over IPv4
|
||||||
|
if d.primary_ip6:
|
||||||
|
d.primary_ip = d.primary_ip6
|
||||||
|
elif d.primary_ip4:
|
||||||
|
d.primary_ip = d.primary_ip4
|
||||||
|
d.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0006_add_device_primary_ip4_ip6'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(copy_primary_ip, restore_primary_ip),
|
||||||
|
]
|
19
netbox/dcim/migrations/0008_device_remove_primary_ip.py
Normal file
19
netbox/dcim/migrations/0008_device_remove_primary_ip.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-11 19:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0007_device_copy_primary_ip'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='device',
|
||||||
|
name='primary_ip',
|
||||||
|
),
|
||||||
|
]
|
@ -263,7 +263,7 @@ class Rack(CreatedUpdatedModel):
|
|||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
if self.facility_id:
|
if self.facility_id:
|
||||||
return "{} ({})".format(self.name, self.facility_id)
|
return u"{} ({})".format(self.name, self.facility_id)
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
|
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
|
||||||
@ -605,8 +605,10 @@ class Device(CreatedUpdatedModel):
|
|||||||
help_text='Number of the lowest U position occupied by the device')
|
help_text='Number of the lowest U position occupied by the device')
|
||||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
||||||
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||||
primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL,
|
primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
|
||||||
blank=True, null=True, verbose_name='Primary IP')
|
blank=True, null=True, verbose_name='Primary IPv4')
|
||||||
|
primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
|
||||||
|
blank=True, null=True, verbose_name='Primary IPv6')
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -696,9 +698,9 @@ class Device(CreatedUpdatedModel):
|
|||||||
if self.name:
|
if self.name:
|
||||||
return self.name
|
return self.name
|
||||||
elif self.position:
|
elif self.position:
|
||||||
return "{} ({} U{})".format(self.device_type, self.rack.name, self.position)
|
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
|
||||||
else:
|
else:
|
||||||
return "{} ({})".format(self.device_type, self.rack.name)
|
return u"{} ({})".format(self.device_type, self.rack.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
@ -709,6 +711,15 @@ class Device(CreatedUpdatedModel):
|
|||||||
return self.name
|
return self.name
|
||||||
return '{{{}}}'.format(self.pk)
|
return '{{{}}}'.format(self.pk)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primary_ip(self):
|
||||||
|
if self.primary_ip6:
|
||||||
|
return self.primary_ip6
|
||||||
|
elif self.primary_ip4:
|
||||||
|
return self.primary_ip4
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_children(self):
|
def get_children(self):
|
||||||
"""
|
"""
|
||||||
Return the set of child Devices installed in DeviceBays within this Device.
|
Return the set of child Devices installed in DeviceBays within this Device.
|
||||||
|
@ -318,6 +318,8 @@ class DeviceTest(APITestCase):
|
|||||||
'parent_device',
|
'parent_device',
|
||||||
'status',
|
'status',
|
||||||
'primary_ip',
|
'primary_ip',
|
||||||
|
'primary_ip4',
|
||||||
|
'primary_ip6',
|
||||||
'comments',
|
'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -375,6 +377,10 @@ class DeviceTest(APITestCase):
|
|||||||
'primary_ip_address',
|
'primary_ip_address',
|
||||||
'primary_ip_family',
|
'primary_ip_family',
|
||||||
'primary_ip_id',
|
'primary_ip_id',
|
||||||
|
'primary_ip4_address',
|
||||||
|
'primary_ip4_family',
|
||||||
|
'primary_ip4_id',
|
||||||
|
'primary_ip6',
|
||||||
'rack_display_name',
|
'rack_display_name',
|
||||||
'rack_facility_id',
|
'rack_facility_id',
|
||||||
'rack_id',
|
'rack_id',
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
from natsort import natsorted
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -13,8 +14,6 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from natsort import natsorted
|
|
||||||
|
|
||||||
from ipam.models import Prefix, IPAddress, VLAN
|
from ipam.models import Prefix, IPAddress, VLAN
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import TopologyMap
|
from extras.models import TopologyMap
|
||||||
@ -262,13 +261,22 @@ def devicetype(request, pk):
|
|||||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||||
|
|
||||||
# Component tables
|
# Component tables
|
||||||
consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype))
|
consoleport_table = tables.ConsolePortTemplateTable(
|
||||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.objects
|
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
.filter(device_type=devicetype))
|
)
|
||||||
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype))
|
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
||||||
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype))
|
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
|
powerport_table = tables.PowerPortTemplateTable(
|
||||||
|
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
|
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||||
|
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
|
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype))
|
devicebay_table = tables.DeviceBayTemplateTable(
|
||||||
|
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
if request.user.has_perm('dcim.change_devicetype'):
|
if request.user.has_perm('dcim.change_devicetype'):
|
||||||
consoleport_table.base_columns['pk'].visible = True
|
consoleport_table.base_columns['pk'].visible = True
|
||||||
consoleserverport_table.base_columns['pk'].visible = True
|
consoleserverport_table.base_columns['pk'].visible = True
|
||||||
@ -504,7 +512,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceListView(ObjectListView):
|
class DeviceListView(ObjectListView):
|
||||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip')
|
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip4',
|
||||||
|
'primary_ip6')
|
||||||
filter = filters.DeviceFilter
|
filter = filters.DeviceFilter
|
||||||
filter_form = forms.DeviceFilterForm
|
filter_form = forms.DeviceFilterForm
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
@ -515,17 +524,25 @@ class DeviceListView(ObjectListView):
|
|||||||
def device(request, pk):
|
def device(request, pk):
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=pk)
|
device = get_object_or_404(Device, pk=pk)
|
||||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
console_ports = natsorted(
|
||||||
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
||||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
)
|
||||||
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
cs_ports = natsorted(
|
||||||
|
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
|
||||||
|
)
|
||||||
|
power_ports = natsorted(
|
||||||
|
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
|
||||||
|
)
|
||||||
|
power_outlets = natsorted(
|
||||||
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
|
)
|
||||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||||
device_bays = natsorted(
|
device_bays = natsorted(
|
||||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||||
key=attrgetter("name")
|
key=attrgetter('name')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gather any secrets which belong to this device
|
# Gather any secrets which belong to this device
|
||||||
@ -1640,7 +1657,10 @@ def ipaddress_assign(request, pk):
|
|||||||
ipaddress.interface))
|
ipaddress.interface))
|
||||||
|
|
||||||
if form.cleaned_data['set_as_primary']:
|
if form.cleaned_data['set_as_primary']:
|
||||||
device.primary_ip = ipaddress
|
if ipaddress.family == 4:
|
||||||
|
device.primary_ip4 = ipaddress
|
||||||
|
elif ipaddress.family == 6:
|
||||||
|
device.primary_ip6 = ipaddress
|
||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
|
@ -329,7 +329,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class IPAddressFromCSVForm(forms.ModelForm):
|
class IPAddressFromCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'VRF not found.'})
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
error_messages={'invalid_choice': 'Device not found.'})
|
||||||
interface_name = forms.CharField(required=False)
|
interface_name = forms.CharField(required=False)
|
||||||
@ -368,7 +368,10 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
name=self.cleaned_data['interface_name'])
|
name=self.cleaned_data['interface_name'])
|
||||||
# Set as primary for device
|
# Set as primary for device
|
||||||
if self.cleaned_data['is_primary']:
|
if self.cleaned_data['is_primary']:
|
||||||
self.instance.primary_for = self.cleaned_data['device']
|
if self.instance.family == 4:
|
||||||
|
self.instance.primary_ip4_for = self.cleaned_data['device']
|
||||||
|
elif self.instance.family == 6:
|
||||||
|
self.instance.primary_ip6_for = self.cleaned_data['device']
|
||||||
|
|
||||||
return super(IPAddressFromCSVForm, self).save(commit=commit)
|
return super(IPAddressFromCSVForm, self).save(commit=commit)
|
||||||
|
|
||||||
|
@ -314,12 +314,20 @@ class IPAddress(CreatedUpdatedModel):
|
|||||||
super(IPAddress, self).save(*args, **kwargs)
|
super(IPAddress, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
|
|
||||||
|
# Determine if this IP is primary for a Device
|
||||||
|
is_primary = False
|
||||||
|
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
|
||||||
|
is_primary = True
|
||||||
|
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
|
||||||
|
is_primary = True
|
||||||
|
|
||||||
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.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 getattr(self, 'primary_for', False) else '',
|
'True' if is_primary else '',
|
||||||
self.description,
|
self.description,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -367,7 +375,7 @@ class VLAN(CreatedUpdatedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
return "{} ({})".format(self.vid, self.name)
|
return u"{} ({})".format(self.vid, self.name)
|
||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
@ -364,7 +364,7 @@ def prefix_ipaddresses(request, pk):
|
|||||||
|
|
||||||
# Find all IPAddresses belonging to this Prefix
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
|
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
|
||||||
.select_related('vrf', 'interface__device', 'primary_for')
|
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||||
|
|
||||||
ip_table = tables.IPAddressTable(ipaddresses)
|
ip_table = tables.IPAddressTable(ipaddresses)
|
||||||
ip_table.model = IPAddress
|
ip_table.model = IPAddress
|
||||||
@ -383,7 +383,7 @@ def prefix_ipaddresses(request, pk):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressListView(ObjectListView):
|
class IPAddressListView(ObjectListView):
|
||||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_for')
|
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||||
filter = filters.IPAddressFilter
|
filter = filters.IPAddressFilter
|
||||||
filter_form = forms.IPAddressFilterForm
|
filter_form = forms.IPAddressFilterForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
@ -443,8 +443,13 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
obj.save()
|
obj.save()
|
||||||
# Update primary IP for device if needed
|
# Update primary IP for device if needed
|
||||||
try:
|
try:
|
||||||
device = obj.primary_for
|
if obj.family == 4 and obj.primary_ip4_for:
|
||||||
device.primary_ip = obj
|
device = obj.primary_ip4_for
|
||||||
|
device.primary_ip4 = obj
|
||||||
|
device.save()
|
||||||
|
elif obj.family == 6 and obj.primary_ip6_for:
|
||||||
|
device = obj.primary_ip6_for
|
||||||
|
device.primary_ip6 = obj
|
||||||
device.save()
|
device.save()
|
||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
@ -73,3 +73,8 @@ TIME_FORMAT = 'g:i a'
|
|||||||
SHORT_TIME_FORMAT = 'H:i:s'
|
SHORT_TIME_FORMAT = 'H:i:s'
|
||||||
DATETIME_FORMAT = 'N j, Y g:i a'
|
DATETIME_FORMAT = 'N j, Y g:i a'
|
||||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
||||||
|
|
||||||
|
# Optionally display a persistent banner at the top and/or bottom of every page. To display the same content in both
|
||||||
|
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||||
|
BANNER_TOP = ''
|
||||||
|
BANNER_BOTTOM = ''
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.1.1-dev'
|
VERSION = '1.2.1-dev'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
@ -38,6 +38,8 @@ TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
|||||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||||
|
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||||
|
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||||
|
|
||||||
# Attempt to import LDAP configuration if it has been defined
|
# Attempt to import LDAP configuration if it has been defined
|
||||||
|
@ -28,6 +28,42 @@ body {
|
|||||||
footer p {
|
footer p {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 1120px) {
|
||||||
|
.navbar-header {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
.navbar-left,.navbar-right {
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
.navbar-toggle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.navbar-collapse {
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.navbar-fixed-top {
|
||||||
|
top: 0;
|
||||||
|
border-width: 0 0 1px;
|
||||||
|
}
|
||||||
|
.navbar-collapse.collapse {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
.navbar-nav {
|
||||||
|
float: none!important;
|
||||||
|
margin-top: 7.5px;
|
||||||
|
}
|
||||||
|
.navbar-nav>li {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
.navbar-nav>li>a {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.collapse.in {
|
||||||
|
display:block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
label {
|
label {
|
||||||
@ -259,6 +295,9 @@ ul.rack_near_face li.empty:hover a {
|
|||||||
.dark_gray:hover { background-color: #2c3e50; }
|
.dark_gray:hover { background-color: #2c3e50; }
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
|
.banner-bottom {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
.panel table {
|
.panel table {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class SecretListView(generics.GenericAPIView):
|
|||||||
"""
|
"""
|
||||||
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
|
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
|
||||||
"""
|
"""
|
||||||
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
|
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
|
||||||
.prefetch_related('role__users', 'role__groups')
|
.prefetch_related('role__users', 'role__groups')
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
filter_class = SecretFilter
|
filter_class = SecretFilter
|
||||||
@ -87,7 +87,7 @@ class SecretDetailView(generics.GenericAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
|
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
|
||||||
"""
|
"""
|
||||||
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
|
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
|
||||||
.prefetch_related('role__users', 'role__groups')
|
.prefetch_related('role__users', 'role__groups')
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||||
|
@ -224,6 +224,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container wrapper">
|
<div class="container wrapper">
|
||||||
|
{% if settings.BANNER_TOP %}
|
||||||
|
<div class="alert alert-info text-center" role="alert">
|
||||||
|
{{ settings.BANNER_TOP|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if settings.MAINTENANCE_MODE %}
|
{% if settings.MAINTENANCE_MODE %}
|
||||||
<div class="alert alert-warning text-center" role="alert">
|
<div class="alert alert-warning text-center" role="alert">
|
||||||
<h4><i class="fa fa-exclamation-triangle"></i> Maintenance Mode</h4>
|
<h4><i class="fa fa-exclamation-triangle"></i> Maintenance Mode</h4>
|
||||||
@ -240,6 +245,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
<div class="push"></div>
|
<div class="push"></div>
|
||||||
|
{% if settings.BANNER_BOTTOM %}
|
||||||
|
<div class="alert alert-info text-center banner-bottom" role="alert">
|
||||||
|
{{ settings.BANNER_BOTTOM|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -101,14 +101,29 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Primary IP</td>
|
<td>Primary IPv4</td>
|
||||||
<td>
|
<td>
|
||||||
{% if device.primary_ip %}
|
{% if device.primary_ip4 %}
|
||||||
<a href="{% url 'ipam:ipaddress' pk=device.primary_ip.pk %}">{{ device.primary_ip.address.ip }}</a>
|
<a href="{% url 'ipam:ipaddress' pk=device.primary_ip4.pk %}">{{ device.primary_ip4.address.ip }}</a>
|
||||||
{% if device.primary_ip.nat_inside %}
|
{% if device.primary_ip4.nat_inside %}
|
||||||
<span>(NAT for {{ device.primary_ip.nat_inside.address.ip }})</span>
|
<span>(NAT for {{ device.primary_ip4.nat_inside.address.ip }})</span>
|
||||||
{% elif device.primary_ip.nat_outside %}
|
{% elif device.primary_ip4.nat_outside %}
|
||||||
<span>(NAT: {{ device.primary_ip.nat_outside.address.ip }})</span>
|
<span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not defined</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Primary IPv6</td>
|
||||||
|
<td>
|
||||||
|
{% if device.primary_ip6 %}
|
||||||
|
<a href="{% url 'ipam:ipaddress' pk=device.primary_ip6.pk %}">{{ device.primary_ip6.address.ip }}</a>
|
||||||
|
{% if device.primary_ip6.nat_inside %}
|
||||||
|
<span>(NAT for {{ device.primary_ip6.nat_inside.address.ip }})</span>
|
||||||
|
{% elif device.primary_ip6.nat_outside %}
|
||||||
|
<span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not defined</span>
|
<span class="text-muted">Not defined</span>
|
||||||
|
@ -31,7 +31,10 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.platform %}
|
{% render_field form.platform %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% if obj %}{% render_field form.primary_ip %}{% endif %}
|
{% if obj %}
|
||||||
|
{% render_field form.primary_ip4 %}
|
||||||
|
{% render_field form.primary_ip6 %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ ip.interface }}</td>
|
<td>{{ ip.interface }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if device.primary_ip == ip %}
|
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||||
<span class="label label-success">Primary</span>
|
<span class="label label-success">Primary</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
Loading…
Reference in New Issue
Block a user