mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-21 12:52:21 -06:00
Merge branch 'develop' of github.com:digitalocean/netbox into develop-2.2
This commit is contained in:
@@ -13,6 +13,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
@@ -642,15 +643,16 @@ class InterfaceQuerySet(models.QuerySet):
|
||||
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{type}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
{type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||
be parsed as follows:
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
|
||||
would be parsed as follows:
|
||||
|
||||
name = 'GigabitEthernet'
|
||||
slot = None
|
||||
subslot = 0
|
||||
position = 1
|
||||
slot = 1
|
||||
subslot = 2
|
||||
position = 3
|
||||
subposition = 0
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
@@ -659,17 +661,35 @@ class InterfaceQuerySet(models.QuerySet):
|
||||
"""
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
|
||||
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
||||
IFACE_ORDERING_POSITION: (
|
||||
'_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', '_id', 'name',
|
||||
),
|
||||
IFACE_ORDERING_NAME: (
|
||||
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
|
||||
),
|
||||
}[method]
|
||||
return self.extra(select={
|
||||
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
|
||||
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||
}).order_by(*ordering)
|
||||
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer)"
|
||||
POSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer)"
|
||||
SUBPOSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
||||
}
|
||||
|
||||
return self.annotate(**fields).order_by(*ordering)
|
||||
|
||||
def connectable(self):
|
||||
"""
|
||||
|
||||
@@ -98,3 +98,112 @@ class RackTestCase(TestCase):
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
||||
|
||||
|
||||
class InterfaceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.site = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=self.site,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
name='Acme',
|
||||
slug='acme'
|
||||
)
|
||||
|
||||
self.device_type = DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='FrameForwarder 2048',
|
||||
slug='ff2048'
|
||||
)
|
||||
self.role = DeviceRole.objects.create(
|
||||
name='Switch',
|
||||
slug='switch',
|
||||
)
|
||||
|
||||
def test_interface_order_natural(self):
|
||||
device1 = Device.objects.create(
|
||||
name='TestSwitch1',
|
||||
device_type=self.device_type,
|
||||
device_role=self.role,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
interface1 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/1'
|
||||
)
|
||||
interface2 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/5/1'
|
||||
)
|
||||
interface3 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/4'
|
||||
)
|
||||
interface4 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/2/4'
|
||||
)
|
||||
interface5 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/2/1'
|
||||
)
|
||||
interface6 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Loopback1'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
list(Interface.objects.all().order_naturally()),
|
||||
[interface1, interface5, interface4, interface3, interface2, interface6]
|
||||
)
|
||||
|
||||
def test_interface_order_natural_subinterfaces(self):
|
||||
device1 = Device.objects.create(
|
||||
name='TestSwitch1',
|
||||
device_type=self.device_type,
|
||||
device_role=self.role,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
interface1 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/3'
|
||||
)
|
||||
interface2 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/2.2'
|
||||
)
|
||||
interface3 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/0.120'
|
||||
)
|
||||
interface4 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/0'
|
||||
)
|
||||
interface5 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/1.117'
|
||||
)
|
||||
interface6 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0'
|
||||
)
|
||||
self.assertEqual(
|
||||
list(Interface.objects.all().order_naturally()),
|
||||
[interface4, interface3, interface5, interface2, interface1, interface6]
|
||||
)
|
||||
|
||||
@@ -29,34 +29,47 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
cf = custom_fields[field_name]
|
||||
try:
|
||||
cf = custom_fields[field_name]
|
||||
except KeyError:
|
||||
raise ValidationError(
|
||||
"Invalid custom field for {} objects: {}".format(content_type, field_name)
|
||||
)
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
# Data validation
|
||||
if value not in [None, '']:
|
||||
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError(
|
||||
"Invalid value for boolean field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
|
||||
field_name, value
|
||||
))
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
"Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
"{}: Choice selections must be passed as integers.".format(field_name)
|
||||
)
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError(
|
||||
"Invalid choice for field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
elif cf.required:
|
||||
raise ValidationError("Required field {} cannot be empty.".format(field_name))
|
||||
|
||||
# Check for missing required fields
|
||||
missing_fields = []
|
||||
|
||||
@@ -98,7 +98,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
# Create the new IP address
|
||||
data = request.data.copy()
|
||||
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
|
||||
data['vrf'] = prefix.vrf
|
||||
data['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||
serializer = serializers.WritableIPAddressSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
@@ -516,6 +516,16 @@ class VLANGroup(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
|
||||
def get_next_available_vid(self):
|
||||
"""
|
||||
Return the first available VLAN ID (1-4094) in the group.
|
||||
"""
|
||||
vids = [vlan['vid'] for vlan in self.vlans.order_by('vid').values('vid')]
|
||||
for i in range(1, 4095):
|
||||
if i not in vids:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@@ -34,7 +34,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@@ -120,6 +120,13 @@ VLAN_ROLE_LINK = """
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@@ -203,10 +210,10 @@ class AggregateTable(BaseTable):
|
||||
|
||||
class AggregateDetailTable(AggregateTable):
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(AggregateTable.Meta):
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -249,10 +256,10 @@ class PrefixTable(BaseTable):
|
||||
|
||||
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
|
||||
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -287,11 +287,12 @@ class AggregateListView(ObjectListView):
|
||||
ipv4_total = 0
|
||||
ipv6_total = 0
|
||||
|
||||
for a in self.queryset:
|
||||
if a.prefix.version == 4:
|
||||
ipv4_total += a.prefix.size
|
||||
elif a.prefix.version == 6:
|
||||
ipv6_total += a.prefix.size / 2 ** 64
|
||||
for aggregate in self.queryset:
|
||||
if aggregate.prefix.version == 6:
|
||||
# Report equivalent /64s for IPv6 to keep things sane
|
||||
ipv6_total += int(aggregate.prefix.size / 2 ** 64)
|
||||
else:
|
||||
ipv4_total += aggregate.prefix.size
|
||||
|
||||
return {
|
||||
'ipv4_total': ipv4_total,
|
||||
@@ -315,7 +316,7 @@ class AggregateView(View):
|
||||
)
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
@@ -474,11 +475,11 @@ class PrefixView(View):
|
||||
child_prefixes = Prefix.objects.filter(
|
||||
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
'site', 'vlan', 'role',
|
||||
).annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
child_prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
|
||||
@@ -38,11 +38,14 @@ ADMINS = [
|
||||
# ['John Doe', 'jdoe@example.com'],
|
||||
]
|
||||
|
||||
# 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.
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
|
||||
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
BANNER_TOP = ''
|
||||
BANNER_BOTTOM = ''
|
||||
|
||||
# Text to include on the login page above the login form. HTML is allowed.
|
||||
BANNER_LOGIN = ''
|
||||
|
||||
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
|
||||
# BASE_PATH = 'netbox/'
|
||||
BASE_PATH = ''
|
||||
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.1.5-dev'
|
||||
VERSION = '2.1.6-dev'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -29,8 +29,9 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
||||
# Import optional configuration parameters
|
||||
ADMINS = getattr(configuration, 'ADMINS', [])
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '')
|
||||
BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '')
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
|
||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
@@ -234,6 +235,10 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'utilities.api.TokenPermissions',
|
||||
),
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'utilities.api.FormlessBrowsableAPIRenderer',
|
||||
),
|
||||
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
||||
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
||||
'PAGE_SIZE': PAGINATE_COUNT,
|
||||
|
||||
@@ -329,13 +329,14 @@ li.occupied + li.available {
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
table.component-list tr.ipaddress td {
|
||||
background-color: #eeffff;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
table.component-list td.subtable {
|
||||
padding: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
table.component-list tr.ipaddress:hover td {
|
||||
background-color: #e6f7f7;
|
||||
table.component-list td.subtable td {
|
||||
border: none;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
|
||||
@@ -53,7 +53,7 @@ $(document).ready(function() {
|
||||
success: function(json) {
|
||||
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||
var neighbor = neighbors[0];
|
||||
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
|
||||
var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1"));
|
||||
var configured_device = row.children('td.configured_device').attr('data');
|
||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||
// Add LLDP neighbors to table
|
||||
@@ -62,7 +62,7 @@ $(document).ready(function() {
|
||||
// Apply colors to rows
|
||||
if (!configured_device && neighbor['hostname']) {
|
||||
row.addClass('info');
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port'].split(".")[0]) {
|
||||
row.addClass('success');
|
||||
} else {
|
||||
row.addClass('danger');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}">
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
@@ -113,41 +113,55 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for ip in iface.ip_addresses.all %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td colspan="3">
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% with iface.ip_addresses.all as ipaddresses %}
|
||||
{% if ipaddresses %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
<td colspan="6" class="subtable">
|
||||
{% else %}
|
||||
<span class="text-muted">Global</span>
|
||||
<td colspan="7" class="subtable">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<table class="table table-hover">
|
||||
{% for ip in ipaddresses %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Global table</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -20,11 +20,18 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}
|
||||
<p class="text-right">IPv4 total: <strong>{{ ipv4_total|intcomma }} /32s</strong></p>
|
||||
<p class="text-right">IPv6 total: <strong>{{ ipv6_total|intcomma }} /64s</strong></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
|
||||
<li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="row" style="margin-top: {% if settings.BANNER_LOGIN %}100{% else %}150{% endif %}px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
{% if settings.BANNER_LOGIN %}
|
||||
<div style="margin-bottom: 25px">
|
||||
{{ settings.BANNER_LOGIN|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework.compat import is_authenticated
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS
|
||||
from rest_framework.renderers import BrowsableAPIRenderer
|
||||
from rest_framework.serializers import Field, ModelSerializer, ValidationError
|
||||
from rest_framework.views import get_view_name as drf_get_view_name
|
||||
|
||||
@@ -206,6 +207,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
return self.default_limit
|
||||
|
||||
|
||||
#
|
||||
# Renderers
|
||||
#
|
||||
|
||||
class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
|
||||
"""
|
||||
Override the built-in BrowsableAPIRenderer to disable HTML forms.
|
||||
"""
|
||||
def show_form_for_method(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
@@ -479,7 +479,7 @@ class BulkEditView(View):
|
||||
template_name = 'utilities/obj_bulk_edit.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get(self):
|
||||
def get(self, request):
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
@@ -627,6 +627,9 @@ class BulkDeleteView(View):
|
||||
template_name = 'utilities/obj_bulk_delete.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get(self, request):
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
|
||||
# Attempt to derive parent object if a parent class has been given
|
||||
|
||||
Reference in New Issue
Block a user