mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-21 12:52:21 -06:00
Merge branch 'develop' into develop-2.7
This commit is contained in:
@@ -11,7 +11,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, F, ProtectedError, Q, Sum
|
||||
from django.db.models import Count, F, ProtectedError, Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from netaddr import AddrFormatError, IPNetwork
|
||||
from netaddr import AddrFormatError, IPNetwork, IPAddress
|
||||
|
||||
from . import lookups
|
||||
from .formfields import IPFormField
|
||||
@@ -23,7 +23,10 @@ class BaseIPField(models.Field):
|
||||
if not value:
|
||||
return value
|
||||
try:
|
||||
return IPNetwork(value)
|
||||
if '/' in str(value):
|
||||
return IPNetwork(value)
|
||||
else:
|
||||
return IPAddress(value)
|
||||
except AddrFormatError as e:
|
||||
raise ValidationError("Invalid IP address format: {}".format(value))
|
||||
except (TypeError, ValueError) as e:
|
||||
@@ -32,6 +35,8 @@ class BaseIPField(models.Field):
|
||||
def get_prep_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
return [str(self.to_python(v)) for v in value]
|
||||
return str(self.to_python(value))
|
||||
|
||||
def form_class(self):
|
||||
@@ -90,5 +95,6 @@ IPAddressField.register_lookup(lookups.NetContainedOrEqual)
|
||||
IPAddressField.register_lookup(lookups.NetContains)
|
||||
IPAddressField.register_lookup(lookups.NetContainsOrEquals)
|
||||
IPAddressField.register_lookup(lookups.NetHost)
|
||||
IPAddressField.register_lookup(lookups.NetIn)
|
||||
IPAddressField.register_lookup(lookups.NetHostContained)
|
||||
IPAddressField.register_lookup(lookups.NetMaskLength)
|
||||
|
||||
@@ -7,7 +7,9 @@ from netaddr.core import AddrFormatError
|
||||
from dcim.models import Device, Interface, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .choices import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
@@ -284,7 +286,7 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
||||
method='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
address = django_filters.CharFilter(
|
||||
address = MultiValueCharFilter(
|
||||
method='filter_address',
|
||||
label='Address',
|
||||
)
|
||||
@@ -371,13 +373,8 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
||||
return queryset.none()
|
||||
|
||||
def filter_address(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
try:
|
||||
# Match address and subnet mask
|
||||
if '/' in value:
|
||||
return queryset.filter(address=value)
|
||||
return queryset.filter(address__net_host=value)
|
||||
return queryset.filter(address__net_in=value)
|
||||
except ValidationError:
|
||||
return queryset.none()
|
||||
|
||||
|
||||
@@ -100,6 +100,42 @@ class NetHost(Lookup):
|
||||
return 'HOST(%s) = %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetIn(Lookup):
|
||||
lookup_name = 'net_in'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
with_mask, without_mask = [], []
|
||||
for address in rhs_params[0]:
|
||||
if '/' in address:
|
||||
with_mask.append(address)
|
||||
else:
|
||||
without_mask.append(address)
|
||||
|
||||
address_in_clause = self.create_in_clause('{} IN ('.format(lhs), len(with_mask))
|
||||
host_in_clause = self.create_in_clause('HOST({}) IN ('.format(lhs), len(without_mask))
|
||||
|
||||
if with_mask and not without_mask:
|
||||
return address_in_clause, with_mask
|
||||
elif not with_mask and without_mask:
|
||||
return host_in_clause, without_mask
|
||||
|
||||
in_clause = '({}) OR ({})'.format(address_in_clause, host_in_clause)
|
||||
with_mask.extend(without_mask)
|
||||
return in_clause, with_mask
|
||||
|
||||
@staticmethod
|
||||
def create_in_clause(clause_part, max_size):
|
||||
clause_elements = [clause_part]
|
||||
for offset in range(0, max_size):
|
||||
if offset > 0:
|
||||
clause_elements.append(', ')
|
||||
clause_elements.append('%s')
|
||||
clause_elements.append(')')
|
||||
return ''.join(clause_elements)
|
||||
|
||||
|
||||
class NetHostContained(Lookup):
|
||||
"""
|
||||
Check for the host portion of an IP address without regard to its mask. This allows us to find e.g. 192.0.2.1/24
|
||||
|
||||
@@ -337,16 +337,19 @@ class IPAddressTestCase(TestCase):
|
||||
IPAddress(family=4, address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(family=4, address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(family=4, address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(family=4, address='10.0.0.1/25', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, role=None),
|
||||
IPAddress(family=6, address='2001:db8::1/64', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(family=6, address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(family=6, address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(family=6, address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(family=6, address='2001:db8::1/65', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, role=None),
|
||||
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_dns_name(self):
|
||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||
@@ -359,20 +362,24 @@ class IPAddressTestCase(TestCase):
|
||||
|
||||
def test_parent(self):
|
||||
params = {'parent': '10.0.0.0/24'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
params = {'parent': '2001:db8::/64'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def filter_address(self):
|
||||
def test_filter_address(self):
|
||||
# Check IPv4 and IPv6, with and without a mask
|
||||
params = {'address': '10.0.0.1/24'}
|
||||
params = {'address': ['10.0.0.1/24']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'address': '10.0.0.1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'address': '2001:db8::1/64'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'address': '2001:db8::1'}
|
||||
params = {'address': ['10.0.0.1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'address': ['10.0.0.1/24', '10.0.0.1/25']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'address': ['2001:db8::1/64']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'address': ['2001:db8::1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'address': ['2001:db8::1/64', '2001:db8::1/65']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_mask_length(self):
|
||||
params = {'mask_length': '24'}
|
||||
@@ -411,7 +418,7 @@ class IPAddressTestCase(TestCase):
|
||||
params = {'assigned_to_interface': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'assigned_to_interface': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]}
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.7-beta2'
|
||||
VERSION = '2.7.0-dev'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
||||
@@ -15,7 +15,7 @@ $('button.toggle-ips').click(function() {
|
||||
$('input.interface-filter').on('input', function() {
|
||||
var filter = new RegExp(this.value);
|
||||
|
||||
for (interface of $(this).closest('form').find('tbody > tr')) {
|
||||
for (interface of $(this).closest('div.panel').find('tbody > tr')) {
|
||||
// Slice off 'interface_' at the start of the ID
|
||||
if (filter && filter.test(interface.id.slice(10))) {
|
||||
// Match the toggle in case the filter now matches the interface
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.models import CustomFieldModel, TaggedItem
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from .exceptions import InvalidKey
|
||||
@@ -346,10 +347,14 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.role and self.device and self.name:
|
||||
try:
|
||||
device = self.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
if self.role and device and self.name:
|
||||
return '{} for {} ({})'.format(self.role, self.device, self.name)
|
||||
# Return role and device if no name is set
|
||||
if self.role and self.device:
|
||||
if self.role and device:
|
||||
return '{} for {}'.format(self.role, self.device)
|
||||
return 'Secret'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<li><a href="{{ objectchange.related_object.get_absolute_url }}changelog/">{{ objectchange.related_object }}</a></li>
|
||||
{% elif objectchange.changed_object.get_absolute_url %}
|
||||
<li><a href="{{ objectchange.changed_object.get_absolute_url }}changelog/">{{ objectchange.changed_object }}</a></li>
|
||||
{% else %}
|
||||
{% elif objectchange.changed_object %}
|
||||
<li>{{ objectchange.changed_object }}</li>
|
||||
{% endif %}
|
||||
<li>{{ objectchange }}</li>
|
||||
@@ -97,7 +97,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='noprint' %}
|
||||
{% include 'panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
|
||||
{% if related_changes_count > related_changes_table.rows|length %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'extras:objectchange_list' %}?request_id={{ objectchange.request_id }}" class="btn btn-primary">See all {{ related_changes_count|add:"1" }} changes</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="progress text-center">
|
||||
{% if utilization < 30 %}<span style="font-size: 12px;">{{ utilization }}%</span>{% endif %}
|
||||
<div class="progress-bar progress-bar-{% if utilization >= danger_threshold %}danger{% elif utilization >= warning_threshold %}warning{% else %}success{% endif %}"
|
||||
role="progressbar" aria-valuenow="{{ utilization }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ utilization }}%">
|
||||
role="progressbar" aria-valuenow="{{ utilization }}" aria-valuemin="0" aria-valuemax="100" style="width: {% if utilization > 100 %}100{% else %}{{ utilization }}{% endif %}%">
|
||||
{% if utilization >= 30 %}{{ utilization }}%{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user