mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
This commit is contained in:
@ -21,6 +21,14 @@ Height: {{ rack.u_height }}U
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
|
||||
```
|
||||
{% for server in queryset %}
|
||||
{% set data = server.get_config_context() %}
|
||||
{{ data.syslog }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.)
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
|
@ -10,7 +10,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's
|
||||
| ----------- | ----------- |
|
||||
| `{"status": "active"}` | Status is active |
|
||||
| `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved |
|
||||
| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing |
|
||||
| `{"status": "active", "role": "testing"}` | Status is active **AND** role is testing |
|
||||
| `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) |
|
||||
| `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) |
|
||||
| `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 |
|
||||
|
@ -66,6 +66,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
|
||||
* `PASSWORD` - Redis password (if set)
|
||||
* `DATABASE` - Numeric database ID
|
||||
* `SSL` - Use SSL connection to Redis
|
||||
* `INSECURE_SKIP_TLS_VERIFY` - Set to `True` to **disable** TLS certificate verification (not recommended)
|
||||
|
||||
An example configuration is provided below:
|
||||
|
||||
|
@ -8,11 +8,17 @@
|
||||
* [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view
|
||||
* [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color
|
||||
* [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant
|
||||
* [#6083](https://github.com/netbox-community/netbox/issues/6083) - Support disabling TLS certificate validation for Redis
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations
|
||||
* [#6070](https://github.com/netbox-community/netbox/issues/6070) - Add missing `count_ipaddresses` attribute to VMInterface serializer
|
||||
* [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission
|
||||
* [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint
|
||||
* [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses
|
||||
* [#6117](https://github.com/netbox-community/netbox/issues/6117) - Handle exception when attempting to assign an MPTT-enabled model as its own parent
|
||||
* [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects
|
||||
|
||||
---
|
||||
|
||||
|
@ -842,7 +842,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
|
||||
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
interface_a = serializers.SerializerMethodField()
|
||||
interface_b = NestedInterfaceSerializer(source='connected_endpoint')
|
||||
interface_b = NestedInterfaceSerializer(source='_path.destination')
|
||||
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -2,6 +2,7 @@ import socket
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import F
|
||||
from django.http import HttpResponseForbidden, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
@ -596,6 +597,8 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = Interface.objects.prefetch_related('device', '_path').filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
_path__destination_type__app_label='dcim',
|
||||
_path__destination_type__model='interface',
|
||||
_path__destination_id__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
)
|
||||
|
@ -507,6 +507,10 @@ class BaseInterface(models.Model):
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def count_ipaddresses(self):
|
||||
return self.ip_addresses.count()
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
@ -674,10 +678,6 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
def is_lag(self):
|
||||
return self.type == InterfaceTypeChoices.TYPE_LAG
|
||||
|
||||
@property
|
||||
def count_ipaddresses(self):
|
||||
return self.ip_addresses.count()
|
||||
|
||||
|
||||
#
|
||||
# Pass-through ports
|
||||
|
@ -7,6 +7,7 @@ from timezone_field import TimeZoneField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from django.core.exceptions import ValidationError
|
||||
from dcim.fields import ASNField
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import NestedGroupModel, PrimaryModel
|
||||
|
@ -34,6 +34,9 @@ REDIS = {
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
@ -44,6 +47,9 @@ REDIS = {
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,6 +195,15 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel, MPTTMo
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# An MPTT model cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({
|
||||
"parent": "Cannot assign self as parent."
|
||||
})
|
||||
|
||||
|
||||
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel):
|
||||
"""
|
||||
|
@ -221,6 +221,7 @@ TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
|
||||
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
|
||||
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
|
||||
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
|
||||
TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
|
||||
|
||||
# Caching
|
||||
if 'caching' not in REDIS:
|
||||
@ -239,6 +240,7 @@ CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default'
|
||||
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
|
||||
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
|
||||
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
|
||||
CACHING_REDIS_SKIP_TLS_VERIFY = CACHING_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
|
||||
|
||||
|
||||
#
|
||||
@ -406,21 +408,14 @@ if CACHING_REDIS_USING_SENTINEL:
|
||||
'password': CACHING_REDIS_PASSWORD,
|
||||
}
|
||||
else:
|
||||
if CACHING_REDIS_SSL:
|
||||
REDIS_CACHE_CON_STRING = 'rediss://'
|
||||
else:
|
||||
REDIS_CACHE_CON_STRING = 'redis://'
|
||||
|
||||
if CACHING_REDIS_PASSWORD:
|
||||
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD)
|
||||
|
||||
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(
|
||||
REDIS_CACHE_CON_STRING,
|
||||
CACHING_REDIS_HOST,
|
||||
CACHING_REDIS_PORT,
|
||||
CACHING_REDIS_DATABASE
|
||||
)
|
||||
CACHEOPS_REDIS = REDIS_CACHE_CON_STRING
|
||||
CACHEOPS_REDIS = {
|
||||
'host': CACHING_REDIS_HOST,
|
||||
'port': CACHING_REDIS_PORT,
|
||||
'db': CACHING_REDIS_DATABASE,
|
||||
'password': CACHING_REDIS_PASSWORD,
|
||||
'ssl': CACHING_REDIS_SSL,
|
||||
'ssl_cert_reqs': None if CACHING_REDIS_SKIP_TLS_VERIFY else 'required',
|
||||
}
|
||||
|
||||
if not CACHE_TIMEOUT:
|
||||
CACHEOPS_ENABLED = False
|
||||
@ -568,6 +563,7 @@ else:
|
||||
'DB': TASKS_REDIS_DATABASE,
|
||||
'PASSWORD': TASKS_REDIS_PASSWORD,
|
||||
'SSL': TASKS_REDIS_SSL,
|
||||
'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
|
||||
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
@ -28,6 +29,7 @@
|
||||
{% block sidebar %}{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
|
||||
{% if permissions.change or permissions.delete %}
|
||||
<form method="post" class="form form-horizontal">
|
||||
@ -57,7 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include table_template|default:'responsive_table.html' %}
|
||||
{% render_table table 'inc/table.html' %}
|
||||
<div class="pull-left noprint">
|
||||
{% block bulk_buttons %}{% endblock %}
|
||||
{% if bulk_edit_url and permissions.change %}
|
||||
@ -73,9 +75,10 @@
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
{% include table_template|default:'responsive_table.html' %}
|
||||
{% render_table table 'inc/table.html' %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
@ -226,12 +226,12 @@ def prepare_cloned_fields(instance):
|
||||
field = instance._meta.get_field(field_name)
|
||||
field_value = field.value_from_object(instance)
|
||||
|
||||
# Swap out False with URL-friendly value
|
||||
# Pass False as null for boolean fields
|
||||
if field_value is False:
|
||||
field_value = ''
|
||||
params.append((field_name, ''))
|
||||
|
||||
# Omit empty values
|
||||
if field_value not in (None, ''):
|
||||
elif field_value not in (None, ''):
|
||||
params.append((field_name, field_value))
|
||||
|
||||
# Copy tags
|
||||
|
@ -115,12 +115,14 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'mtu', 'mac_address', 'description',
|
||||
'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'count_ipaddresses',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
@ -80,7 +80,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
|
||||
|
||||
class VMInterfaceViewSet(ModelViewSet):
|
||||
queryset = VMInterface.objects.prefetch_related(
|
||||
'virtual_machine', 'parent', 'tags', 'tagged_vlans'
|
||||
'virtual_machine', 'parent', 'tags', 'tagged_vlans', 'ip_addresses'
|
||||
)
|
||||
serializer_class = serializers.VMInterfaceSerializer
|
||||
filterset_class = filters.VMInterfaceFilterSet
|
||||
|
@ -477,7 +477,3 @@ class VMInterface(PrimaryModel, BaseInterface):
|
||||
@property
|
||||
def parent_object(self):
|
||||
return self.virtual_machine
|
||||
|
||||
@property
|
||||
def count_ipaddresses(self):
|
||||
return self.ip_addresses.count()
|
||||
|
Reference in New Issue
Block a user