Просмотр исходного кода

Merge branch 'develop' into feature

jeremystretch 4 лет назад
Родитель
Сommit
b0573f88e6

+ 8 - 0
docs/additional-features/export-templates.md

@@ -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`.

+ 1 - 1
docs/administration/permissions.md

@@ -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 |

+ 1 - 0
docs/configuration/required-settings.md

@@ -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:
 

+ 6 - 0
docs/release-notes/version-2.10.md

@@ -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
 
 ---
 

+ 1 - 1
netbox/dcim/api/serializers.py

@@ -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:

+ 3 - 0
netbox/dcim/api/views.py

@@ -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')
     )

+ 4 - 4
netbox/dcim/models/device_components.py

@@ -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

+ 1 - 0
netbox/dcim/models/sites.py

@@ -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

+ 6 - 0
netbox/netbox/configuration.example.py

@@ -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,
     }
 }
 

+ 9 - 0
netbox/netbox/models.py

@@ -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):
     """

+ 11 - 15
netbox/netbox/settings.py

@@ -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,
     }
 

+ 46 - 43
netbox/templates/generic/object_list.html

@@ -1,6 +1,7 @@
 {% extends 'base.html' %}
 {% load buttons %}
 {% load helpers %}
+{% load render_table from django_tables2 %}
 {% load static %}
 
 {% block content %}
@@ -28,54 +29,56 @@
                 {% 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">
-                {% csrf_token %}
-                <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
-                {% if table.paginator.num_pages > 1 %}
-                    <div id="select_all_box" class="hidden panel panel-default noprint">
-                        <div class="panel-body">
-                            <div class="checkbox-inline">
-                                <label for="select_all">
-                                    <input type="checkbox" id="select_all" name="_all" />
-                                    Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
-                                </label>
-                            </div>
-                            <div class="pull-right">
-                                {% if bulk_edit_url and permissions.change %}
-                                    <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
-                                        <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
-                                    </button>
-                                {% endif %}
-                                {% if bulk_delete_url and permissions.delete %}
-                                    <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
-                                        <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
-                                    </button>
-                                {% endif %}
+            {% if permissions.change or permissions.delete %}
+                <form method="post" class="form form-horizontal">
+                    {% csrf_token %}
+                    <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
+                    {% if table.paginator.num_pages > 1 %}
+                        <div id="select_all_box" class="hidden panel panel-default noprint">
+                            <div class="panel-body">
+                                <div class="checkbox-inline">
+                                    <label for="select_all">
+                                        <input type="checkbox" id="select_all" name="_all" />
+                                        Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
+                                    </label>
+                                </div>
+                                <div class="pull-right">
+                                    {% if bulk_edit_url and permissions.change %}
+                                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
+                                            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
+                                        </button>
+                                    {% endif %}
+                                    {% if bulk_delete_url and permissions.delete %}
+                                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
+                                            <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
+                                        </button>
+                                    {% endif %}
+                                </div>
                             </div>
                         </div>
-                    </div>
-                {% endif %}
-                {% include table_template|default:'responsive_table.html' %}
-                <div class="pull-left noprint">
-                    {% block bulk_buttons %}{% endblock %}
-                    {% if bulk_edit_url and permissions.change %}
-                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
-                            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit Selected
-                        </button>
-                    {% endif %}
-                    {% if bulk_delete_url and permissions.delete %}
-                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
-                            <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete Selected
-                        </button>
                     {% endif %}
-                </div>
-            </form>
-        {% else %}
-            {% include table_template|default:'responsive_table.html' %}
-        {% endif %}
+                    {% render_table table 'inc/table.html' %}
+                    <div class="pull-left noprint">
+                        {% block bulk_buttons %}{% endblock %}
+                        {% if bulk_edit_url and permissions.change %}
+                            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
+                                <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit Selected
+                            </button>
+                        {% endif %}
+                        {% if bulk_delete_url and permissions.delete %}
+                            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
+                                <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete Selected
+                            </button>
+                        {% endif %}
+                    </div>
+                </form>
+            {% else %}
+                {% 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 - 0
netbox/tenancy/models.py

@@ -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

+ 3 - 3
netbox/utilities/utils.py

@@ -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

+ 2 - 0
netbox/virtualization/api/serializers.py

@@ -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):

+ 1 - 1
netbox/virtualization/api/views.py

@@ -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

+ 0 - 4
netbox/virtualization/models.py

@@ -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()