From 3711283de53bfc7950ab56e6312f76578ee2d344 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 6 May 2020 23:43:46 -0400
Subject: [PATCH 01/43] Extend ViewTestCases to get and list objects as a
non-authenticated user
---
netbox/utilities/testing/testcases.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/netbox/utilities/testing/testcases.py b/netbox/utilities/testing/testcases.py
index de8b93232..d10bb025a 100644
--- a/netbox/utilities/testing/testcases.py
+++ b/netbox/utilities/testing/testcases.py
@@ -164,6 +164,13 @@ class ViewTestCases:
response = self.client.get(instance.get_absolute_url())
self.assertHttpStatus(response, 200)
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_list_objects_anonymous(self):
+ # Make the request as an unauthenticated user
+ self.client.logout()
+ response = self.client.get(self.model.objects.first().get_absolute_url())
+ self.assertHttpStatus(response, 200)
+
class CreateObjectViewTestCase(ModelViewTestCase):
"""
Create a single new instance.
@@ -287,6 +294,13 @@ class ViewTestCases:
self.assertHttpStatus(response, 200)
self.assertEqual(response.get('Content-Type'), 'text/csv')
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_list_objects_anonymous(self):
+ # Make the request as an unauthenticated user
+ self.client.logout()
+ response = self.client.get(self._get_url('list'))
+ self.assertHttpStatus(response, 200)
+
class BulkCreateObjectsViewTestCase(ModelViewTestCase):
"""
Create multiple instances using a single form. Expects the creation of three new instances by default.
From 5c1adf9e3775968842f6c0deddd7291ac924b554 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 6 May 2020 23:44:06 -0400
Subject: [PATCH 02/43] Fixes #4593: Fix AttributeError exception when viewing
object lists as a non-authenticated user
---
docs/release-notes/version-2.8.md | 8 ++++++++
netbox/extras/views.py | 14 ++++++++++----
netbox/templates/utilities/obj_list.html | 2 +-
netbox/utilities/paginator.py | 7 +++++--
netbox/utilities/views.py | 8 +++++++-
5 files changed, 31 insertions(+), 8 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index e75bf4ab9..62d5e9920 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,5 +1,13 @@
# NetBox v2.8
+## v2.8.3 (FUTURE)
+
+### Bug Fixes
+
+* [#4593](https://github.com/netbox-community/netbox/issues/4593) - Fix AttributeError exception when viewing object lists as a non-authenticated user
+
+---
+
## v2.8.2 (2020-05-06)
### Enhancements
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 613e45132..1bfbb7abf 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -124,9 +124,12 @@ class ConfigContextView(PermissionRequiredMixin, View):
# Determine user's preferred output format
if request.GET.get('format') in ['json', 'yaml']:
format = request.GET.get('format')
- request.user.config.set('extras.configcontext.format', format, commit=True)
- else:
+ if request.user.is_authenticated:
+ request.user.config.set('extras.configcontext.format', format, commit=True)
+ elif request.user.is_authenticated:
format = request.user.config.get('extras.configcontext.format', 'json')
+ else:
+ format = 'json'
return render(request, 'extras/configcontext.html', {
'configcontext': configcontext,
@@ -181,9 +184,12 @@ class ObjectConfigContextView(View):
# Determine user's preferred output format
if request.GET.get('format') in ['json', 'yaml']:
format = request.GET.get('format')
- request.user.config.set('extras.configcontext.format', format, commit=True)
- else:
+ if request.user.is_authenticated:
+ request.user.config.set('extras.configcontext.format', format, commit=True)
+ elif request.user.is_authenticated:
format = request.user.config.get('extras.configcontext.format', 'json')
+ else:
+ format = 'json'
return render(request, 'extras/object_configcontext.html', {
model_name: obj,
diff --git a/netbox/templates/utilities/obj_list.html b/netbox/templates/utilities/obj_list.html
index 4cfa8b1ce..85ff050ed 100644
--- a/netbox/templates/utilities/obj_list.html
+++ b/netbox/templates/utilities/obj_list.html
@@ -5,7 +5,7 @@
{% block content %}
{% block buttons %}{% endblock %}
- {% if table_config_form %}
+ {% if request.user.is_authenticated and table_config_form %}
{% endif %}
{% if permissions.add and 'add' in action_buttons %}
diff --git a/netbox/utilities/paginator.py b/netbox/utilities/paginator.py
index cef7c941f..cdad1f230 100644
--- a/netbox/utilities/paginator.py
+++ b/netbox/utilities/paginator.py
@@ -50,9 +50,12 @@ def get_paginate_count(request):
if 'per_page' in request.GET:
try:
per_page = int(request.GET.get('per_page'))
- request.user.config.set('pagination.per_page', per_page, commit=True)
+ if request.user.is_authenticated:
+ request.user.config.set('pagination.per_page', per_page, commit=True)
return per_page
except ValueError:
pass
- return request.user.config.get('pagination.per_page', settings.PAGINATE_COUNT)
+ if request.user.is_authenticated:
+ return request.user.config.get('pagination.per_page', settings.PAGINATE_COUNT)
+ return settings.PAGINATE_COUNT
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py
index 3064abe4e..4b5993c5f 100644
--- a/netbox/utilities/views.py
+++ b/netbox/utilities/views.py
@@ -3,6 +3,7 @@ import sys
from copy import deepcopy
from django.contrib import messages
+from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
@@ -13,6 +14,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist
from django.urls import reverse
+from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
@@ -164,7 +166,10 @@ class ObjectListView(View):
permissions[action] = request.user.has_perm(perm_name)
# Construct the table based on the user's permissions
- columns = request.user.config.get(f"tables.{self.table.__name__}.columns")
+ if request.user.is_authenticated:
+ columns = request.user.config.get(f"tables.{self.table.__name__}.columns")
+ else:
+ columns = None
table = self.table(self.queryset, columns=columns)
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
table.columns.show('pk')
@@ -188,6 +193,7 @@ class ObjectListView(View):
return render(request, self.template_name, context)
+ @method_decorator(login_required)
def post(self, request):
# Update the user's table configuration
From af96ffb3e9d0ceb0dfa1e67b77d8628b466c3abf Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 6 May 2020 23:46:52 -0400
Subject: [PATCH 03/43] Release v2.8.3
---
docs/release-notes/version-2.8.md | 2 +-
netbox/netbox/settings.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 62d5e9920..e3bd6b512 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,6 +1,6 @@
# NetBox v2.8
-## v2.8.3 (FUTURE)
+## v2.8.3 (2020-05-06)
### Bug Fixes
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index ce352ebda..415c556e9 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
-VERSION = '2.8.3-dev'
+VERSION = '2.8.3'
# Hostname
HOSTNAME = platform.node()
From 7c6faff405d4c4a0877362a048ccc9313e671cbf Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 6 May 2020 23:50:41 -0400
Subject: [PATCH 04/43] Post-release version bump
---
netbox/netbox/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 415c556e9..f928ca71e 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
-VERSION = '2.8.3'
+VERSION = '2.8.4-dev'
# Hostname
HOSTNAME = platform.node()
From b7a96a33efe25e84a0e8ffebc3d3280dda0ae9c9 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Thu, 7 May 2020 10:34:33 -0400
Subject: [PATCH 05/43] Fixes #4598: Display error message when invalid cable
length is specified
---
docs/release-notes/version-2.8.md | 8 ++++++++
netbox/dcim/forms.py | 5 +++++
netbox/templates/dcim/inc/cable_form.html | 14 ++++++++++++++
3 files changed, 27 insertions(+)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index e3bd6b512..e1dfaddbb 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,5 +1,13 @@
# NetBox v2.8
+v2.8.4 (FUTURE)
+
+### Bug Fixes
+
+* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
+
+---
+
## v2.8.3 (2020-05-06)
### Bug Fixes
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index b104124b4..2116d0948 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -3659,6 +3659,11 @@ class CableForm(BootstrapMixin, forms.ModelForm):
'type': StaticSelect2,
'length_unit': StaticSelect2,
}
+ error_messages = {
+ 'length': {
+ 'max_value': 'Maximum length is 32767 (any unit)'
+ }
+ }
class CableCSVForm(CSVModelForm):
diff --git a/netbox/templates/dcim/inc/cable_form.html b/netbox/templates/dcim/inc/cable_form.html
index 0799eb130..a52cc302e 100644
--- a/netbox/templates/dcim/inc/cable_form.html
+++ b/netbox/templates/dcim/inc/cable_form.html
@@ -10,9 +10,23 @@
{{ form.length }}
+ {% if form.length.errors %}
+
+ {% for error in form.length.errors %}
+
{{ error }}
+ {% endfor %}
+
+ {% endif %}
{{ form.length_unit }}
+ {% if form.length_unit.errors %}
+
+ {% for error in form.length_unit.errors %}
+
{{ error }}
+ {% endfor %}
+
+ {% endif %}
From e14e217fcd2544441fce3028b7ea2f37fa55cc18 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Thu, 7 May 2020 16:22:04 -0400
Subject: [PATCH 06/43] Fixes #4604: Multi-position rear ports may only be
connected to other rear ports
---
docs/release-notes/version-2.8.md | 1 +
netbox/dcim/models/__init__.py | 30 ++++++++++++++++++------------
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index e1dfaddbb..ff6ba4e50 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -5,6 +5,7 @@ v2.8.4 (FUTURE)
### Bug Fixes
* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
+* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
---
diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py
index b0da352da..490667153 100644
--- a/netbox/dcim/models/__init__.py
+++ b/netbox/dcim/models/__init__.py
@@ -2182,23 +2182,29 @@ class Cable(ChangeLoggedModel):
# Check that termination types are compatible
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
- raise ValidationError("Incompatible termination types: {} and {}".format(
- self.termination_a_type, self.termination_b_type
- ))
+ raise ValidationError(
+ f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
+ )
- # A RearPort with multiple positions must be connected to a component with an equal number of positions
- if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
- if self.termination_a.positions != self.termination_b.positions:
- raise ValidationError(
- "{} has {} positions and {} has {}. Both terminations must have the same number of positions.".format(
- self.termination_a, self.termination_a.positions,
- self.termination_b, self.termination_b.positions
+ # A RearPort with multiple positions must be connected to a RearPort with an equal number of positions
+ for term_a, term_b in [
+ (self.termination_a, self.termination_b),
+ (self.termination_b, self.termination_a)
+ ]:
+ if isinstance(term_a, RearPort) and term_a.positions > 1:
+ if not isinstance(term_b, RearPort):
+ raise ValidationError(
+ "Rear ports with multiple positions may only be connected to other rear ports"
+ )
+ elif term_a.positions != term_b.positions:
+ raise ValidationError(
+ f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. "
+ f"Both terminations must have the same number of positions."
)
- )
# A termination point cannot be connected to itself
if self.termination_a == self.termination_b:
- raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
+ raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
# A front port cannot be connected to its corresponding rear port
if (
From da8380c62cd9ef256dc3017fc991d7061d1e2199 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Thu, 7 May 2020 16:59:27 -0400
Subject: [PATCH 07/43] Refactor extras.models
---
.../migrations/0006_add_imageattachments.py | 4 +-
.../migrations/0007_unicode_literals.py | 4 +-
netbox/extras/models/__init__.py | 25 ++
netbox/extras/models/customfields.py | 297 ++++++++++++++
netbox/extras/{ => models}/models.py | 374 +-----------------
netbox/extras/models/tags.py | 44 +++
netbox/extras/utils.py | 16 +
7 files changed, 390 insertions(+), 374 deletions(-)
create mode 100644 netbox/extras/models/__init__.py
create mode 100644 netbox/extras/models/customfields.py
rename netbox/extras/{ => models}/models.py (63%)
create mode 100644 netbox/extras/models/tags.py
diff --git a/netbox/extras/migrations/0006_add_imageattachments.py b/netbox/extras/migrations/0006_add_imageattachments.py
index 6842cced0..b25327c33 100644
--- a/netbox/extras/migrations/0006_add_imageattachments.py
+++ b/netbox/extras/migrations/0006_add_imageattachments.py
@@ -2,7 +2,7 @@
# Generated by Django 1.11 on 2017-04-04 19:58
from django.db import migrations, models
import django.db.models.deletion
-import extras.models
+import extras.utils
class Migration(migrations.Migration):
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
- ('image', models.ImageField(height_field=b'image_height', upload_to=extras.models.image_upload, width_field=b'image_width')),
+ ('image', models.ImageField(height_field=b'image_height', upload_to=extras.utils.image_upload, width_field=b'image_width')),
('image_height', models.PositiveSmallIntegerField()),
('image_width', models.PositiveSmallIntegerField()),
('name', models.CharField(blank=True, max_length=50)),
diff --git a/netbox/extras/migrations/0007_unicode_literals.py b/netbox/extras/migrations/0007_unicode_literals.py
index fecb33b7b..88525a24a 100644
--- a/netbox/extras/migrations/0007_unicode_literals.py
+++ b/netbox/extras/migrations/0007_unicode_literals.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from django.db import migrations, models
-import extras.models
+import extras.utils
class Migration(migrations.Migration):
@@ -74,7 +74,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='imageattachment',
name='image',
- field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'),
+ field=models.ImageField(height_field='image_height', upload_to=extras.utils.image_upload, width_field='image_width'),
),
migrations.AlterField(
model_name='topologymap',
diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py
new file mode 100644
index 000000000..2942bfa48
--- /dev/null
+++ b/netbox/extras/models/__init__.py
@@ -0,0 +1,25 @@
+from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
+from .models import (
+ ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult,
+ Script, Webhook,
+)
+from .tags import Tag, TaggedItem
+
+__all__ = (
+ 'ConfigContext',
+ 'ConfigContextModel',
+ 'CustomField',
+ 'CustomFieldChoice',
+ 'CustomFieldModel',
+ 'CustomFieldValue',
+ 'CustomLink',
+ 'ExportTemplate',
+ 'Graph',
+ 'ImageAttachment',
+ 'ObjectChange',
+ 'ReportResult',
+ 'Script',
+ 'Tag',
+ 'TaggedItem',
+ 'Webhook',
+)
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
new file mode 100644
index 000000000..47bccd98a
--- /dev/null
+++ b/netbox/extras/models/customfields.py
@@ -0,0 +1,297 @@
+from collections import OrderedDict
+from datetime import date
+
+from django import forms
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.core.validators import ValidationError
+from django.db import models
+
+from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
+from extras.choices import *
+from extras.utils import FeatureQuery
+
+
+#
+# Custom fields
+#
+
+class CustomFieldModel(models.Model):
+ _cf = None
+
+ class Meta:
+ abstract = True
+
+ def cache_custom_fields(self):
+ """
+ Cache all custom field values for this instance
+ """
+ self._cf = {
+ field.name: value for field, value in self.get_custom_fields().items()
+ }
+
+ @property
+ def cf(self):
+ """
+ Name-based CustomFieldValue accessor for use in templates
+ """
+ if self._cf is None:
+ self.cache_custom_fields()
+ return self._cf
+
+ def get_custom_fields(self):
+ """
+ Return a dictionary of custom fields for a single object in the form {: value}.
+ """
+
+ # Find all custom fields applicable to this type of object
+ content_type = ContentType.objects.get_for_model(self)
+ fields = CustomField.objects.filter(obj_type=content_type)
+
+ # If the object exists, populate its custom fields with values
+ if hasattr(self, 'pk'):
+ values = self.custom_field_values.all()
+ values_dict = {cfv.field_id: cfv.value for cfv in values}
+ return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
+ else:
+ return OrderedDict([(field, None) for field in fields])
+
+
+class CustomField(models.Model):
+ obj_type = models.ManyToManyField(
+ to=ContentType,
+ related_name='custom_fields',
+ verbose_name='Object(s)',
+ limit_choices_to=FeatureQuery('custom_fields'),
+ help_text='The object(s) to which this field applies.'
+ )
+ type = models.CharField(
+ max_length=50,
+ choices=CustomFieldTypeChoices,
+ default=CustomFieldTypeChoices.TYPE_TEXT
+ )
+ name = models.CharField(
+ max_length=50,
+ unique=True
+ )
+ label = models.CharField(
+ max_length=50,
+ blank=True,
+ help_text='Name of the field as displayed to users (if not provided, '
+ 'the field\'s name will be used)'
+ )
+ description = models.CharField(
+ max_length=200,
+ blank=True
+ )
+ required = models.BooleanField(
+ default=False,
+ help_text='If true, this field is required when creating new objects '
+ 'or editing an existing object.'
+ )
+ filter_logic = models.CharField(
+ max_length=50,
+ choices=CustomFieldFilterLogicChoices,
+ default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
+ help_text='Loose matches any instance of a given string; exact '
+ 'matches the entire field.'
+ )
+ default = models.CharField(
+ max_length=100,
+ blank=True,
+ help_text='Default value for the field. Use "true" or "false" for booleans.'
+ )
+ weight = models.PositiveSmallIntegerField(
+ default=100,
+ help_text='Fields with higher weights appear lower in a form.'
+ )
+
+ class Meta:
+ ordering = ['weight', 'name']
+
+ def __str__(self):
+ return self.label or self.name.replace('_', ' ').capitalize()
+
+ def serialize_value(self, value):
+ """
+ Serialize the given value to a string suitable for storage as a CustomFieldValue
+ """
+ if value is None:
+ return ''
+ if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
+ return str(int(bool(value)))
+ if self.type == CustomFieldTypeChoices.TYPE_DATE:
+ # Could be date/datetime object or string
+ try:
+ return value.strftime('%Y-%m-%d')
+ except AttributeError:
+ return value
+ if self.type == CustomFieldTypeChoices.TYPE_SELECT:
+ # Could be ModelChoiceField or TypedChoiceField
+ return str(value.id) if hasattr(value, 'id') else str(value)
+ return value
+
+ def deserialize_value(self, serialized_value):
+ """
+ Convert a string into the object it represents depending on the type of field
+ """
+ if serialized_value == '':
+ return None
+ if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
+ return int(serialized_value)
+ if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
+ return bool(int(serialized_value))
+ if self.type == CustomFieldTypeChoices.TYPE_DATE:
+ # Read date as YYYY-MM-DD
+ return date(*[int(n) for n in serialized_value.split('-')])
+ if self.type == CustomFieldTypeChoices.TYPE_SELECT:
+ return self.choices.get(pk=int(serialized_value))
+ return serialized_value
+
+ def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
+ """
+ Return a form field suitable for setting a CustomField's value for an object.
+
+ set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
+ enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
+ for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
+ """
+ initial = self.default if set_initial else None
+ required = self.required if enforce_required else False
+
+ # Integer
+ if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
+ field = forms.IntegerField(required=required, initial=initial)
+
+ # Boolean
+ elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
+ choices = (
+ (None, '---------'),
+ (1, 'True'),
+ (0, 'False'),
+ )
+ if initial is not None and initial.lower() in ['true', 'yes', '1']:
+ initial = 1
+ elif initial is not None and initial.lower() in ['false', 'no', '0']:
+ initial = 0
+ else:
+ initial = None
+ field = forms.NullBooleanField(
+ required=required, initial=initial, widget=StaticSelect2(choices=choices)
+ )
+
+ # Date
+ elif self.type == CustomFieldTypeChoices.TYPE_DATE:
+ field = forms.DateField(required=required, initial=initial, widget=DatePicker())
+
+ # Select
+ elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
+ choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
+
+ if not required:
+ choices = add_blank_choice(choices)
+
+ # Set the initial value to the PK of the default choice, if any
+ if set_initial:
+ default_choice = self.choices.filter(value=self.default).first()
+ if default_choice:
+ initial = default_choice.pk
+
+ field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
+ field = field_class(
+ choices=choices, required=required, initial=initial, widget=StaticSelect2()
+ )
+
+ # URL
+ elif self.type == CustomFieldTypeChoices.TYPE_URL:
+ field = LaxURLField(required=required, initial=initial)
+
+ # Text
+ else:
+ field = forms.CharField(max_length=255, required=required, initial=initial)
+
+ field.model = self
+ field.label = self.label if self.label else self.name.replace('_', ' ').capitalize()
+ if self.description:
+ field.help_text = self.description
+
+ return field
+
+
+class CustomFieldValue(models.Model):
+ field = models.ForeignKey(
+ to='extras.CustomField',
+ on_delete=models.CASCADE,
+ related_name='values'
+ )
+ obj_type = models.ForeignKey(
+ to=ContentType,
+ on_delete=models.PROTECT,
+ related_name='+'
+ )
+ obj_id = models.PositiveIntegerField()
+ obj = GenericForeignKey(
+ ct_field='obj_type',
+ fk_field='obj_id'
+ )
+ serialized_value = models.CharField(
+ max_length=255
+ )
+
+ class Meta:
+ ordering = ('obj_type', 'obj_id', 'pk') # (obj_type, obj_id) may be non-unique
+ unique_together = ('field', 'obj_type', 'obj_id')
+
+ def __str__(self):
+ return '{} {}'.format(self.obj, self.field)
+
+ @property
+ def value(self):
+ return self.field.deserialize_value(self.serialized_value)
+
+ @value.setter
+ def value(self, value):
+ self.serialized_value = self.field.serialize_value(value)
+
+ def save(self, *args, **kwargs):
+ # Delete this object if it no longer has a value to store
+ if self.pk and self.value is None:
+ self.delete()
+ else:
+ super().save(*args, **kwargs)
+
+
+class CustomFieldChoice(models.Model):
+ field = models.ForeignKey(
+ to='extras.CustomField',
+ on_delete=models.CASCADE,
+ related_name='choices',
+ limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
+ )
+ value = models.CharField(
+ max_length=100
+ )
+ weight = models.PositiveSmallIntegerField(
+ default=100,
+ help_text='Higher weights appear lower in the list'
+ )
+
+ class Meta:
+ ordering = ['field', 'weight', 'value']
+ unique_together = ['field', 'value']
+
+ def __str__(self):
+ return self.value
+
+ def clean(self):
+ if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
+ raise ValidationError("Custom field choices can only be assigned to selection fields.")
+
+ def delete(self, using=None, keep_parents=False):
+ # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
+ pk = self.pk
+ super().delete(using, keep_parents)
+ CustomFieldValue.objects.filter(
+ field__type=CustomFieldTypeChoices.TYPE_SELECT,
+ serialized_value=str(pk)
+ ).delete()
diff --git a/netbox/extras/models.py b/netbox/extras/models/models.py
similarity index 63%
rename from netbox/extras/models.py
rename to netbox/extras/models/models.py
index 488554596..f98a7b34f 100644
--- a/netbox/extras/models.py
+++ b/netbox/extras/models/models.py
@@ -1,8 +1,6 @@
import json
from collections import OrderedDict
-from datetime import date
-from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -12,37 +10,13 @@ from django.db import models
from django.http import HttpResponse
from django.template import Template, Context
from django.urls import reverse
-from django.utils.text import slugify
from rest_framework.utils.encoders import JSONEncoder
-from taggit.models import TagBase, GenericTaggedItemBase
-from utilities.fields import ColorField
-from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
from utilities.utils import deepmerge, render_jinja2
-from .choices import *
-from .constants import *
-from .querysets import ConfigContextQuerySet
-from .utils import FeatureQuery
-
-
-__all__ = (
- 'ConfigContext',
- 'ConfigContextModel',
- 'CustomField',
- 'CustomFieldChoice',
- 'CustomFieldModel',
- 'CustomFieldValue',
- 'CustomLink',
- 'ExportTemplate',
- 'Graph',
- 'ImageAttachment',
- 'ObjectChange',
- 'ReportResult',
- 'Script',
- 'Tag',
- 'TaggedItem',
- 'Webhook',
-)
+from extras.choices import *
+from extras.constants import *
+from extras.querysets import ConfigContextQuerySet
+from extras.utils import FeatureQuery, image_upload
#
@@ -174,291 +148,6 @@ class Webhook(models.Model):
return json.dumps(context, cls=JSONEncoder)
-#
-# Custom fields
-#
-
-class CustomFieldModel(models.Model):
- _cf = None
-
- class Meta:
- abstract = True
-
- def cache_custom_fields(self):
- """
- Cache all custom field values for this instance
- """
- self._cf = {
- field.name: value for field, value in self.get_custom_fields().items()
- }
-
- @property
- def cf(self):
- """
- Name-based CustomFieldValue accessor for use in templates
- """
- if self._cf is None:
- self.cache_custom_fields()
- return self._cf
-
- def get_custom_fields(self):
- """
- Return a dictionary of custom fields for a single object in the form {: value}.
- """
-
- # Find all custom fields applicable to this type of object
- content_type = ContentType.objects.get_for_model(self)
- fields = CustomField.objects.filter(obj_type=content_type)
-
- # If the object exists, populate its custom fields with values
- if hasattr(self, 'pk'):
- values = self.custom_field_values.all()
- values_dict = {cfv.field_id: cfv.value for cfv in values}
- return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
- else:
- return OrderedDict([(field, None) for field in fields])
-
-
-class CustomField(models.Model):
- obj_type = models.ManyToManyField(
- to=ContentType,
- related_name='custom_fields',
- verbose_name='Object(s)',
- limit_choices_to=FeatureQuery('custom_fields'),
- help_text='The object(s) to which this field applies.'
- )
- type = models.CharField(
- max_length=50,
- choices=CustomFieldTypeChoices,
- default=CustomFieldTypeChoices.TYPE_TEXT
- )
- name = models.CharField(
- max_length=50,
- unique=True
- )
- label = models.CharField(
- max_length=50,
- blank=True,
- help_text='Name of the field as displayed to users (if not provided, '
- 'the field\'s name will be used)'
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- required = models.BooleanField(
- default=False,
- help_text='If true, this field is required when creating new objects '
- 'or editing an existing object.'
- )
- filter_logic = models.CharField(
- max_length=50,
- choices=CustomFieldFilterLogicChoices,
- default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
- help_text='Loose matches any instance of a given string; exact '
- 'matches the entire field.'
- )
- default = models.CharField(
- max_length=100,
- blank=True,
- help_text='Default value for the field. Use "true" or "false" for booleans.'
- )
- weight = models.PositiveSmallIntegerField(
- default=100,
- help_text='Fields with higher weights appear lower in a form.'
- )
-
- class Meta:
- ordering = ['weight', 'name']
-
- def __str__(self):
- return self.label or self.name.replace('_', ' ').capitalize()
-
- def serialize_value(self, value):
- """
- Serialize the given value to a string suitable for storage as a CustomFieldValue
- """
- if value is None:
- return ''
- if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
- return str(int(bool(value)))
- if self.type == CustomFieldTypeChoices.TYPE_DATE:
- # Could be date/datetime object or string
- try:
- return value.strftime('%Y-%m-%d')
- except AttributeError:
- return value
- if self.type == CustomFieldTypeChoices.TYPE_SELECT:
- # Could be ModelChoiceField or TypedChoiceField
- return str(value.id) if hasattr(value, 'id') else str(value)
- return value
-
- def deserialize_value(self, serialized_value):
- """
- Convert a string into the object it represents depending on the type of field
- """
- if serialized_value == '':
- return None
- if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
- return int(serialized_value)
- if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
- return bool(int(serialized_value))
- if self.type == CustomFieldTypeChoices.TYPE_DATE:
- # Read date as YYYY-MM-DD
- return date(*[int(n) for n in serialized_value.split('-')])
- if self.type == CustomFieldTypeChoices.TYPE_SELECT:
- return self.choices.get(pk=int(serialized_value))
- return serialized_value
-
- def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
- """
- Return a form field suitable for setting a CustomField's value for an object.
-
- set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
- enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
- for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
- """
- initial = self.default if set_initial else None
- required = self.required if enforce_required else False
-
- # Integer
- if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
- field = forms.IntegerField(required=required, initial=initial)
-
- # Boolean
- elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
- choices = (
- (None, '---------'),
- (1, 'True'),
- (0, 'False'),
- )
- if initial is not None and initial.lower() in ['true', 'yes', '1']:
- initial = 1
- elif initial is not None and initial.lower() in ['false', 'no', '0']:
- initial = 0
- else:
- initial = None
- field = forms.NullBooleanField(
- required=required, initial=initial, widget=StaticSelect2(choices=choices)
- )
-
- # Date
- elif self.type == CustomFieldTypeChoices.TYPE_DATE:
- field = forms.DateField(required=required, initial=initial, widget=DatePicker())
-
- # Select
- elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
- choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
-
- if not required:
- choices = add_blank_choice(choices)
-
- # Set the initial value to the PK of the default choice, if any
- if set_initial:
- default_choice = self.choices.filter(value=self.default).first()
- if default_choice:
- initial = default_choice.pk
-
- field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
- field = field_class(
- choices=choices, required=required, initial=initial, widget=StaticSelect2()
- )
-
- # URL
- elif self.type == CustomFieldTypeChoices.TYPE_URL:
- field = LaxURLField(required=required, initial=initial)
-
- # Text
- else:
- field = forms.CharField(max_length=255, required=required, initial=initial)
-
- field.model = self
- field.label = self.label if self.label else self.name.replace('_', ' ').capitalize()
- if self.description:
- field.help_text = self.description
-
- return field
-
-
-class CustomFieldValue(models.Model):
- field = models.ForeignKey(
- to='extras.CustomField',
- on_delete=models.CASCADE,
- related_name='values'
- )
- obj_type = models.ForeignKey(
- to=ContentType,
- on_delete=models.PROTECT,
- related_name='+'
- )
- obj_id = models.PositiveIntegerField()
- obj = GenericForeignKey(
- ct_field='obj_type',
- fk_field='obj_id'
- )
- serialized_value = models.CharField(
- max_length=255
- )
-
- class Meta:
- ordering = ('obj_type', 'obj_id', 'pk') # (obj_type, obj_id) may be non-unique
- unique_together = ('field', 'obj_type', 'obj_id')
-
- def __str__(self):
- return '{} {}'.format(self.obj, self.field)
-
- @property
- def value(self):
- return self.field.deserialize_value(self.serialized_value)
-
- @value.setter
- def value(self, value):
- self.serialized_value = self.field.serialize_value(value)
-
- def save(self, *args, **kwargs):
- # Delete this object if it no longer has a value to store
- if self.pk and self.value is None:
- self.delete()
- else:
- super().save(*args, **kwargs)
-
-
-class CustomFieldChoice(models.Model):
- field = models.ForeignKey(
- to='extras.CustomField',
- on_delete=models.CASCADE,
- related_name='choices',
- limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
- )
- value = models.CharField(
- max_length=100
- )
- weight = models.PositiveSmallIntegerField(
- default=100,
- help_text='Higher weights appear lower in the list'
- )
-
- class Meta:
- ordering = ['field', 'weight', 'value']
- unique_together = ['field', 'value']
-
- def __str__(self):
- return self.value
-
- def clean(self):
- if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
- raise ValidationError("Custom field choices can only be assigned to selection fields.")
-
- def delete(self, using=None, keep_parents=False):
- # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
- pk = self.pk
- super().delete(using, keep_parents)
- CustomFieldValue.objects.filter(
- field__type=CustomFieldTypeChoices.TYPE_SELECT,
- serialized_value=str(pk)
- ).delete()
-
-
#
# Custom links
#
@@ -663,20 +352,6 @@ class ExportTemplate(models.Model):
# Image attachments
#
-def image_upload(instance, filename):
-
- path = 'image-attachments/'
-
- # Rename the file to the provided name, if any. Attempt to preserve the file extension.
- extension = filename.rsplit('.')[-1].lower()
- if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
- filename = '.'.join([instance.name, extension])
- elif instance.name:
- filename = instance.name
-
- return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
-
-
class ImageAttachment(models.Model):
"""
An uploaded image which is associated with an object.
@@ -1038,44 +713,3 @@ class ObjectChange(models.Model):
self.object_repr,
self.object_data,
)
-
-
-#
-# Tags
-#
-
-# TODO: figure out a way around this circular import for ObjectChange
-from utilities.models import ChangeLoggedModel # noqa: E402
-
-
-class Tag(TagBase, ChangeLoggedModel):
- color = ColorField(
- default='9e9e9e'
- )
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- def get_absolute_url(self):
- return reverse('extras:tag', args=[self.slug])
-
- def slugify(self, tag, i=None):
- # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
- slug = slugify(tag, allow_unicode=True)
- if i is not None:
- slug += "_%d" % i
- return slug
-
-
-class TaggedItem(GenericTaggedItemBase):
- tag = models.ForeignKey(
- to=Tag,
- related_name="%(app_label)s_%(class)s_items",
- on_delete=models.CASCADE
- )
-
- class Meta:
- index_together = (
- ("content_type", "object_id")
- )
diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py
new file mode 100644
index 000000000..3bad7fa8b
--- /dev/null
+++ b/netbox/extras/models/tags.py
@@ -0,0 +1,44 @@
+from django.db import models
+from django.urls import reverse
+from django.utils.text import slugify
+from taggit.models import TagBase, GenericTaggedItemBase
+
+from utilities.fields import ColorField
+from utilities.models import ChangeLoggedModel
+
+
+#
+# Tags
+#
+
+class Tag(TagBase, ChangeLoggedModel):
+ color = ColorField(
+ default='9e9e9e'
+ )
+ description = models.CharField(
+ max_length=200,
+ blank=True,
+ )
+
+ def get_absolute_url(self):
+ return reverse('extras:tag', args=[self.slug])
+
+ def slugify(self, tag, i=None):
+ # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
+ slug = slugify(tag, allow_unicode=True)
+ if i is not None:
+ slug += "_%d" % i
+ return slug
+
+
+class TaggedItem(GenericTaggedItemBase):
+ tag = models.ForeignKey(
+ to=Tag,
+ related_name="%(app_label)s_%(class)s_items",
+ on_delete=models.CASCADE
+ )
+
+ class Meta:
+ index_together = (
+ ("content_type", "object_id")
+ )
diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py
index 78214fe41..edbd509f1 100644
--- a/netbox/extras/utils.py
+++ b/netbox/extras/utils.py
@@ -22,6 +22,22 @@ def is_taggable(obj):
return False
+def image_upload(instance, filename):
+ """
+ Return a path for uploading image attchments.
+ """
+ path = 'image-attachments/'
+
+ # Rename the file to the provided name, if any. Attempt to preserve the file extension.
+ extension = filename.rsplit('.')[-1].lower()
+ if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
+ filename = '.'.join([instance.name, extension])
+ elif instance.name:
+ filename = instance.name
+
+ return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
+
+
@deconstructible
class FeatureQuery:
"""
From 2c19390d7c69c3c930def43153567b3cd807a379 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Thu, 7 May 2020 17:20:32 -0400
Subject: [PATCH 08/43] Introduce CustomFieldManager (WIP)
---
.../migrations/0042_customfield_manager.py | 20 ++++++++++
netbox/extras/models/customfields.py | 38 +++++++++++++++++--
2 files changed, 54 insertions(+), 4 deletions(-)
create mode 100644 netbox/extras/migrations/0042_customfield_manager.py
diff --git a/netbox/extras/migrations/0042_customfield_manager.py b/netbox/extras/migrations/0042_customfield_manager.py
new file mode 100644
index 000000000..7d80b567a
--- /dev/null
+++ b/netbox/extras/migrations/0042_customfield_manager.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.0.5 on 2020-05-07 21:06
+
+from django.db import migrations
+import extras.models.customfields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0041_tag_description'),
+ ]
+
+ operations = [
+ migrations.AlterModelManagers(
+ name='customfield',
+ managers=[
+ ('objects', extras.models.customfields.CustomFieldManager()),
+ ],
+ ),
+ ]
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index 47bccd98a..f00a52a5d 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -43,10 +43,7 @@ class CustomFieldModel(models.Model):
"""
Return a dictionary of custom fields for a single object in the form {: value}.
"""
-
- # Find all custom fields applicable to this type of object
- content_type = ContentType.objects.get_for_model(self)
- fields = CustomField.objects.filter(obj_type=content_type)
+ fields = CustomField.objects.get_for_model(self)
# If the object exists, populate its custom fields with values
if hasattr(self, 'pk'):
@@ -57,6 +54,37 @@ class CustomFieldModel(models.Model):
return OrderedDict([(field, None) for field in fields])
+class CustomFieldManager(models.Manager):
+ use_in_migrations = True
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Initialize a cache for fetched CustomFields
+ self._cache = {}
+
+ def get_for_model(self, model):
+ """
+ Return all CustomFields assigned to the given model.
+ """
+ model = model._meta.concrete_model
+
+ # First try to return from cache
+ try:
+ return self._cache[model]
+ except KeyError:
+ pass
+
+ # Fetch from the database if the model's CustomFields have not been cached
+ content_type = ContentType.objects.get_for_model(model)
+ customfields = CustomField.objects.filter(obj_type=content_type)
+
+ # Cache the retrieved CustomFields
+ self._cache[model] = customfields
+
+ return customfields
+
+
class CustomField(models.Model):
obj_type = models.ManyToManyField(
to=ContentType,
@@ -106,6 +134,8 @@ class CustomField(models.Model):
help_text='Fields with higher weights appear lower in a form.'
)
+ objects = CustomFieldManager()
+
class Meta:
ordering = ['weight', 'name']
From e3be5f84684b9c0f032d6aea830a1792b38db411 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 8 May 2020 10:05:05 -0400
Subject: [PATCH 09/43] Remove local caching attempt
---
netbox/extras/models/customfields.py | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index f00a52a5d..f3e217039 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -1,3 +1,4 @@
+import logging
from collections import OrderedDict
from datetime import date
@@ -57,31 +58,16 @@ class CustomFieldModel(models.Model):
class CustomFieldManager(models.Manager):
use_in_migrations = True
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Initialize a cache for fetched CustomFields
- self._cache = {}
-
def get_for_model(self, model):
"""
Return all CustomFields assigned to the given model.
"""
model = model._meta.concrete_model
- # First try to return from cache
- try:
- return self._cache[model]
- except KeyError:
- pass
-
# Fetch from the database if the model's CustomFields have not been cached
content_type = ContentType.objects.get_for_model(model)
customfields = CustomField.objects.filter(obj_type=content_type)
- # Cache the retrieved CustomFields
- self._cache[model] = customfields
-
return customfields
From 745c9a9c2b8cb1286dad19314f3b08d17c56f057 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 8 May 2020 12:18:08 -0400
Subject: [PATCH 10/43] Add test for CustomFieldManager.get_for_model()
---
netbox/extras/models/customfields.py | 9 ++-------
netbox/extras/tests/test_customfields.py | 13 +++++++++++++
2 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index f3e217039..62e2ca4df 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -62,13 +62,8 @@ class CustomFieldManager(models.Manager):
"""
Return all CustomFields assigned to the given model.
"""
- model = model._meta.concrete_model
-
- # Fetch from the database if the model's CustomFields have not been cached
- content_type = ContentType.objects.get_for_model(model)
- customfields = CustomField.objects.filter(obj_type=content_type)
-
- return customfields
+ content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
+ return self.get_queryset().filter(obj_type=content_type)
class CustomField(models.Model):
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index d76532437..c94d8cd3f 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -99,6 +99,19 @@ class CustomFieldTest(TestCase):
cf.delete()
+class CustomFieldManagerTest(TestCase):
+
+ def setUp(self):
+ content_type = ContentType.objects.get_for_model(Site)
+ custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
+ custom_field.save()
+ custom_field.obj_type.set([content_type])
+
+ def test_get_for_model(self):
+ self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
+ self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
+
+
class CustomFieldAPITest(APITestCase):
@classmethod
From 465d3ae1af4e4129d091776ed1e68d64e12bceeb Mon Sep 17 00:00:00 2001
From: kobayashi
Date: Sat, 9 May 2020 23:06:24 -0400
Subject: [PATCH 11/43] Fix: 4607 Missing token context help
---
docs/api/authentication.md | 13 +------------
docs/models/users/token.md | 12 ++++++++++++
docs/release-notes/version-2.8.md | 1 +
3 files changed, 14 insertions(+), 12 deletions(-)
create mode 100644 docs/models/users/token.md
diff --git a/docs/api/authentication.md b/docs/api/authentication.md
index 8e38c4de9..e8e6ddc96 100644
--- a/docs/api/authentication.md
+++ b/docs/api/authentication.md
@@ -2,18 +2,7 @@
The NetBox API employs token-based authentication. For convenience, cookie authentication can also be used when navigating the browsable API.
-## Tokens
-
-A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
-
-!!! note
- The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
-
-Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
-
-By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
-
-Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
+{!docs/models/users/token.md!}
## Authenticating to the API
diff --git a/docs/models/users/token.md b/docs/models/users/token.md
new file mode 100644
index 000000000..bbeb2284b
--- /dev/null
+++ b/docs/models/users/token.md
@@ -0,0 +1,12 @@
+## Tokens
+
+A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
+
+!!! note
+ The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
+
+Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
+
+By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
+
+Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index ff6ba4e50..aea825ce3 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -6,6 +6,7 @@ v2.8.4 (FUTURE)
* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
+* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
---
From cea01e037a1e9f2e6ec3006fb1b3c24119e427ab Mon Sep 17 00:00:00 2001
From: weisdd <46579601+weisdd@users.noreply.github.com>
Date: Mon, 11 May 2020 16:14:25 +0300
Subject: [PATCH 12/43] Fix: incorrect DeviceConnectionsReport in reports.md
(#4606)
Since the CONNECTION_STATUS_PLANNED constant is gone from dcim.constants, the DeviceConnectionsReport script is no longer correct.
The suggested fix is based on the fact that console_port.connection_status and power_port.connection_status currently have the following set of values:
* None = A cable is not connected to a Console Server Port or it's connected to a Rear/Front Port;
* False = A cable is connected to a Console Server Port and marked as Planned;
* True = A cable is connected to a Console Server Port and marked as Installed.
---
docs/additional-features/reports.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/docs/additional-features/reports.md b/docs/additional-features/reports.md
index 6deddc140..e845117c0 100644
--- a/docs/additional-features/reports.md
+++ b/docs/additional-features/reports.md
@@ -33,7 +33,6 @@ Within each report class, we'll create a number of test methods to execute our r
```
from dcim.choices import DeviceStatusChoices
-from dcim.constants import CONNECTION_STATUS_PLANNED
from dcim.models import ConsolePort, Device, PowerPort
from extras.reports import Report
@@ -51,7 +50,7 @@ class DeviceConnectionsReport(Report):
console_port.device,
"No console connection defined for {}".format(console_port.name)
)
- elif console_port.connection_status == CONNECTION_STATUS_PLANNED:
+ elif not console_port.connection_status:
self.log_warning(
console_port.device,
"Console connection for {} marked as planned".format(console_port.name)
@@ -67,7 +66,7 @@ class DeviceConnectionsReport(Report):
for power_port in PowerPort.objects.filter(device=device):
if power_port.connected_endpoint is not None:
connected_ports += 1
- if power_port.connection_status == CONNECTION_STATUS_PLANNED:
+ if not power_port.connection_status:
self.log_warning(
device,
"Power connection for {} marked as planned".format(power_port.name)
From 41361ce2a2cd8ace762be10d891f31572dcbec51 Mon Sep 17 00:00:00 2001
From: Daniel Sheppard
Date: Mon, 11 May 2020 16:10:23 -0500
Subject: [PATCH 13/43] Fixes: #4618 - Add group creation and correct user
creation group syntax
---
docs/installation/3-netbox.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md
index 5237e617e..c583d08fe 100644
--- a/docs/installation/3-netbox.md
+++ b/docs/installation/3-netbox.md
@@ -78,7 +78,8 @@ Create a system user account named `netbox`. We'll configure the WSGI and HTTP s
CentOS users may need to create the `netbox` group first.
```
-# adduser --system --group netbox
+# groupadd --system netbox
+# adduser --system --gid netbox netbox
# chown --recursive netbox /opt/netbox/netbox/media/
```
From 1d93d9a63ad8f105d18bbac2a26dff4699fbb92f Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 08:53:29 -0400
Subject: [PATCH 14/43] Fixes #4633: Bump django-rq to v2.3.2 to fix
ImportError with rq 1.4.0
---
docs/release-notes/version-2.8.md | 1 +
requirements.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index aea825ce3..416ac2bc6 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -7,6 +7,7 @@ v2.8.4 (FUTURE)
* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
+* [#4633](https://github.com/netbox-community/netbox/issues/4633) - Bump django-rq to v2.3.2 to fix ImportError with rq 1.4.0
---
diff --git a/requirements.txt b/requirements.txt
index c9f51cff0..79e4fdd9f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,7 @@ django-filter==2.2.0
django-mptt==0.11.0
django-pglocks==1.0.4
django-prometheus==2.0.0
-django-rq==2.3.1
+django-rq==2.3.2
django-tables2==2.3.1
django-taggit==1.2.0
django-taggit-serializer==0.1.7
From 569d4ee201bb61d0310283014cbe7a9a3d10f04f Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 09:20:24 -0400
Subject: [PATCH 15/43] Closes #4632: Extend email configuration parameters to
support SSL/TLS
---
docs/configuration/optional-settings.md | 20 ++++++++++++--------
docs/release-notes/version-2.8.md | 4 ++++
netbox/netbox/configuration.example.py | 2 ++
netbox/netbox/settings.py | 8 ++++++--
4 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md
index 3f2b29b87..4d5251f25 100644
--- a/docs/configuration/optional-settings.md
+++ b/docs/configuration/optional-settings.md
@@ -108,16 +108,20 @@ The file path to NetBox's documentation. This is used when presenting context-se
## EMAIL
-In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting:
+In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` configuration parameter:
-* SERVER - Host name or IP address of the email server (use `localhost` if running locally)
-* PORT - TCP port to use for the connection (default: 25)
-* USERNAME - Username with which to authenticate
-* PASSSWORD - Password with which to authenticate
-* TIMEOUT - Amount of time to wait for a connection (seconds)
-* FROM_EMAIL - Sender address for emails sent by NetBox
+* `SERVER` - Host name or IP address of the email server (use `localhost` if running locally)
+* `PORT` - TCP port to use for the connection (default: `25`)
+* `USERNAME` - Username with which to authenticate
+* `PASSSWORD` - Password with which to authenticate
+* `USE_SSL` - Use SSL when connecting to the server (default: `False`). Mutually exclusive with `USE_TLS`.
+* `USE_TLS` - Use TLS when connecting to the server (default: `False`). Mutually exclusive with `USE_SSL`.
+* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)
+* `SSL_KEYFILE` - Path to the PEM-formatted SSL private key file (optional)
+* `TIMEOUT` - Amount of time to wait for a connection, in seconds (default: `10`)
+* `FROM_EMAIL` - Sender address for emails sent by NetBox (default: `root@localhost`)
-Email is sent from NetBox only for critical events. If you would like to test the email server configuration please use the django function [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail):
+Email is sent from NetBox only for critical events or if configured for [logging](#logging). If you would like to test the email server configuration please use the django function [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail):
```
# python ./manage.py nbshell
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 416ac2bc6..e15df481d 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -2,6 +2,10 @@
v2.8.4 (FUTURE)
+### Enhancements
+
+* [#4632](https://github.com/netbox-community/netbox/issues/4632) - Extend email configuration parameters to support SSL/TLS
+
### Bug Fixes
* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py
index 94497f3cd..a020c4322 100644
--- a/netbox/netbox/configuration.example.py
+++ b/netbox/netbox/configuration.example.py
@@ -108,6 +108,8 @@ EMAIL = {
'PORT': 25,
'USERNAME': '',
'PASSWORD': '',
+ 'USE_SSL': False,
+ 'USE_TLS': False,
'TIMEOUT': 10, # seconds
'FROM_EMAIL': '',
}
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index f928ca71e..0162fabd0 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -246,12 +246,16 @@ if SESSION_FILE_PATH is not None:
#
EMAIL_HOST = EMAIL.get('SERVER')
-EMAIL_PORT = EMAIL.get('PORT', 25)
EMAIL_HOST_USER = EMAIL.get('USERNAME')
EMAIL_HOST_PASSWORD = EMAIL.get('PASSWORD')
+EMAIL_PORT = EMAIL.get('PORT', 25)
+EMAIL_SSL_CERTFILE = EMAIL.get('SSL_CERTFILE')
+EMAIL_SSL_KEYFILE = EMAIL.get('SSL_KEYFILE')
+EMAIL_SUBJECT_PREFIX = '[NetBox] '
+EMAIL_USE_SSL = EMAIL.get('USE_SSL', False)
+EMAIL_USE_TLS = EMAIL.get('USE_TLS', False)
EMAIL_TIMEOUT = EMAIL.get('TIMEOUT', 10)
SERVER_EMAIL = EMAIL.get('FROM_EMAIL')
-EMAIL_SUBJECT_PREFIX = '[NetBox] '
#
From 1461be20041b520b98166cf3129f94ab45acbe0f Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 10:28:48 -0400
Subject: [PATCH 16/43] Fixes #4613: Fix tag assignment on config contexts
(regression from #4527)
---
docs/release-notes/version-2.8.md | 1 +
netbox/circuits/forms.py | 2 +-
netbox/dcim/forms.py | 3 +--
netbox/extras/forms.py | 11 ++++++++++-
netbox/ipam/forms.py | 2 +-
netbox/project-static/js/forms.js | 16 ++++++++--------
netbox/secrets/forms.py | 2 +-
netbox/tenancy/forms.py | 2 +-
netbox/virtualization/forms.py | 2 +-
9 files changed, 25 insertions(+), 16 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index e15df481d..7cc05a9f7 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -11,6 +11,7 @@ v2.8.4 (FUTURE)
* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
+* [#4613](https://github.com/netbox-community/netbox/issues/4613) - Fix tag assignment on config contexts (regression from #4527)
* [#4633](https://github.com/netbox-community/netbox/issues/4633) - Bump django-rq to v2.3.2 to fix ImportError with rq 1.4.0
---
diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py
index 427dc2e89..2185d1eab 100644
--- a/netbox/circuits/forms.py
+++ b/netbox/circuits/forms.py
@@ -1,9 +1,9 @@
from django import forms
-from taggit.forms import TagField
from dcim.models import Region, Site
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
+ TagField,
)
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index 2116d0948..5d3ec1019 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -9,13 +9,12 @@ from django.utils.safestring import mark_safe
from mptt.forms import TreeNodeChoiceField
from netaddr import EUI
from netaddr.core import AddrFormatError
-from taggit.forms import TagField
from timezone_field import TimeZoneFormField
from circuits.models import Circuit, Provider
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm,
- LocalConfigContextFilterForm,
+ LocalConfigContextFilterForm, TagField,
)
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from ipam.models import IPAddress, VLAN
diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py
index 384b3563b..469b55efd 100644
--- a/netbox/extras/forms.py
+++ b/netbox/extras/forms.py
@@ -2,7 +2,7 @@ from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from mptt.forms import TreeNodeMultipleChoiceField
-from taggit.forms import TagField
+from taggit.forms import TagField as TagField_
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
@@ -142,6 +142,15 @@ class CustomFieldFilterForm(forms.Form):
# Tags
#
+class TagField(TagField_):
+
+ def widget_attrs(self, widget):
+ # Apply the "tagfield" CSS class to trigger the special API-based selection widget for tags
+ return {
+ 'class': 'tagfield'
+ }
+
+
class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py
index 4e5a413dc..f5fd6e5f8 100644
--- a/netbox/ipam/forms.py
+++ b/netbox/ipam/forms.py
@@ -1,10 +1,10 @@
from django import forms
from django.core.validators import MaxValueValidator, MinValueValidator
-from taggit.forms import TagField
from dcim.models import Device, Interface, Rack, Region, Site
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
+ TagField,
)
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js
index 06d4a742a..b97981f0e 100644
--- a/netbox/project-static/js/forms.js
+++ b/netbox/project-static/js/forms.js
@@ -292,9 +292,9 @@ $(document).ready(function() {
});
// API backed tags
- var tags = $('#id_tags');
+ var tags = $('#id_tags.tagfield');
if (tags.length > 0 && tags.val().length > 0){
- tags = $('#id_tags').val().split(/,\s*/);
+ tags = $('#id_tags.tagfield').val().split(/,\s*/);
} else {
tags = [];
}
@@ -306,8 +306,8 @@ $(document).ready(function() {
}
});
// Replace the django issued text input with a select element
- $('#id_tags').replaceWith('');
- $('#id_tags').select2({
+ $('#id_tags.tagfield').replaceWith('');
+ $('#id_tags.tagfield').select2({
tags: true,
data: tag_objs,
multiple: true,
@@ -354,14 +354,14 @@ $(document).ready(function() {
}
}
});
- $('#id_tags').closest('form').submit(function(event){
+ $('#id_tags.tagfield').closest('form').submit(function(event){
// django-taggit can only accept a single comma seperated string value
- var value = $('#id_tags').val();
+ var value = $('#id_tags.tagfield').val();
if (value.length > 0){
var final_tags = value.join(', ');
- $('#id_tags').val(null).trigger('change');
+ $('#id_tags.tagfield').val(null).trigger('change');
var option = new Option(final_tags, final_tags, true, true);
- $('#id_tags').append(option).trigger('change');
+ $('#id_tags.tagfield').append(option).trigger('change');
}
});
diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py
index 368a47590..089771bd8 100644
--- a/netbox/secrets/forms.py
+++ b/netbox/secrets/forms.py
@@ -1,11 +1,11 @@
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from django import forms
-from taggit.forms import TagField
from dcim.models import Device
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
+ TagField,
)
from utilities.forms import (
APISelectMultiple, BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py
index 700d88b1d..bf100f43a 100644
--- a/netbox/tenancy/forms.py
+++ b/netbox/tenancy/forms.py
@@ -1,8 +1,8 @@
from django import forms
-from taggit.forms import TagField
from extras.forms import (
AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelCSVForm,
+ TagField,
)
from utilities.forms import (
APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm,
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index 0983b2432..2f2ee4950 100644
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -1,6 +1,5 @@
from django import forms
from django.core.exceptions import ValidationError
-from taggit.forms import TagField
from dcim.choices import InterfaceModeChoices
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
@@ -8,6 +7,7 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
+ TagField,
)
from ipam.models import IPAddress, VLAN
from tenancy.forms import TenancyFilterForm, TenancyForm
From fd0be35d99df6376736f5ced1a08984be4519f87 Mon Sep 17 00:00:00 2001
From: Daniel Sheppard
Date: Wed, 13 May 2020 09:26:56 -0500
Subject: [PATCH 17/43] #4634 - Correct inventory item table accessor
definition on manufacturer column
---
docs/release-notes/version-2.8.md | 1 +
netbox/dcim/tables.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index e15df481d..a3249915b 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -12,6 +12,7 @@ v2.8.4 (FUTURE)
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
* [#4633](https://github.com/netbox-community/netbox/issues/4633) - Bump django-rq to v2.3.2 to fix ImportError with rq 1.4.0
+* [#4634](https://github.com/netbox-community/netbox/issues/4634) - Inventory Item List view exception caused by incorrect accessor definition
---
diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
index 3fef86394..9018625a0 100644
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -1195,7 +1195,7 @@ class InventoryItemTable(BaseTable):
args=[Accessor('device.pk')]
)
manufacturer = tables.Column(
- accessor=Accessor('manufacturer.name')
+ accessor=Accessor('manufacturer')
)
discovered = BooleanColumn()
From 07fd92cd4c97b3535cf30cb7314737ad50bb0d24 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 16:25:22 -0400
Subject: [PATCH 18/43] Fixes #4629: Replicate assigned interface when cloning
IP addresses
---
docs/release-notes/version-2.8.md | 1 +
netbox/ipam/models.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 2c8ac7927..914f17cf7 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -12,6 +12,7 @@ v2.8.4 (FUTURE)
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
* [#4613](https://github.com/netbox-community/netbox/issues/4613) - Fix tag assignment on config contexts (regression from #4527)
+* [#4629](https://github.com/netbox-community/netbox/issues/4629) - Replicate assigned interface when cloning IP addresses
* [#4633](https://github.com/netbox-community/netbox/issues/4633) - Bump django-rq to v2.3.2 to fix ImportError with rq 1.4.0
* [#4634](https://github.com/netbox-community/netbox/issues/4634) - Inventory Item List view exception caused by incorrect accessor definition
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index 84720845e..eeb985b7c 100644
--- a/netbox/ipam/models.py
+++ b/netbox/ipam/models.py
@@ -640,7 +640,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
'dns_name', 'description',
]
clone_fields = [
- 'vrf', 'tenant', 'status', 'role', 'description',
+ 'vrf', 'tenant', 'status', 'role', 'description', 'interface',
]
STATUS_CLASS_MAP = {
From 96e05fb12d73badfd32865de3cd8f6542352df26 Mon Sep 17 00:00:00 2001
From: Tyler Bigler
Date: Mon, 11 May 2020 11:18:20 -0400
Subject: [PATCH 19/43] Notes on multiprocessing and gunicorn vs uwsgi
---
docs/additional-features/prometheus-metrics.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/additional-features/prometheus-metrics.md b/docs/additional-features/prometheus-metrics.md
index 0aa944b74..448f925e0 100644
--- a/docs/additional-features/prometheus-metrics.md
+++ b/docs/additional-features/prometheus-metrics.md
@@ -32,3 +32,7 @@ This can be setup by first creating a shared directory and then adding this line
```
environment=prometheus_multiproc_dir=/tmp/prometheus_metrics
```
+
+#### Accuracy
+
+If having long-term-accurate metrics in a multiprocess environment is important to you then it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using Netbox w/ `gunicorn` in a containerized enviroment following the 1-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [this issue](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
\ No newline at end of file
From e0ebb8e7d894ab57594f37baceb9ee1b6624927a Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 17:08:48 -0400
Subject: [PATCH 20/43] Fixes #4617: Restore IP prefix depth notation in list
view
---
docs/release-notes/version-2.8.md | 1 +
netbox/ipam/tables.py | 2 ++
netbox/utilities/tables.py | 9 +++++++--
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 914f17cf7..17e04dad3 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -12,6 +12,7 @@ v2.8.4 (FUTURE)
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
* [#4613](https://github.com/netbox-community/netbox/issues/4613) - Fix tag assignment on config contexts (regression from #4527)
+* [#4617](https://github.com/netbox-community/netbox/issues/4617) - Restore IP prefix depth notation in list view
* [#4629](https://github.com/netbox-community/netbox/issues/4629) - Replicate assigned interface when cloning IP addresses
* [#4633](https://github.com/netbox-community/netbox/issues/4633) - Bump django-rq to v2.3.2 to fix ImportError with rq 1.4.0
* [#4634](https://github.com/netbox-community/netbox/issues/4634) - Inventory Item List view exception caused by incorrect accessor definition
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index 23bf14653..d8b50c11d 100644
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -378,6 +378,8 @@ class PrefixTable(BaseTable):
verbose_name='Pool'
)
+ add_prefetch = False
+
class Meta(BaseTable.Meta):
model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description')
diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py
index 0702936b5..97108b5b2 100644
--- a/netbox/utilities/tables.py
+++ b/netbox/utilities/tables.py
@@ -1,6 +1,5 @@
import django_tables2 as tables
from django.core.exceptions import FieldDoesNotExist
-from django.db.models import ForeignKey
from django.db.models.fields.related import RelatedField
from django.utils.safestring import mark_safe
from django_tables2.data import TableQuerysetData
@@ -9,7 +8,13 @@ from django_tables2.data import TableQuerysetData
class BaseTable(tables.Table):
"""
Default table for object lists
+
+ :param add_prefetch: By default, modify the queryset passed to the table upon initialization to automatically
+ prefetch related data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to
+ accommodate PrefixQuerySet.annotate_depth()).
"""
+ add_prefetch = True
+
class Meta:
attrs = {
'class': 'table table-hover table-headings',
@@ -50,7 +55,7 @@ class BaseTable(tables.Table):
self.sequence.append('actions')
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
- if isinstance(self.data, TableQuerysetData):
+ if self.add_prefetch and isinstance(self.data, TableQuerysetData):
model = getattr(self.Meta, 'model')
prefetch_fields = []
for column in self.columns:
From 29abcbced8780fab47e8cd85c50e2b012953e0aa Mon Sep 17 00:00:00 2001
From: Tyler Bigler
Date: Wed, 13 May 2020 17:13:41 -0400
Subject: [PATCH 21/43] Grammar improvements
---
docs/additional-features/prometheus-metrics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/additional-features/prometheus-metrics.md b/docs/additional-features/prometheus-metrics.md
index 448f925e0..1429fb0a7 100644
--- a/docs/additional-features/prometheus-metrics.md
+++ b/docs/additional-features/prometheus-metrics.md
@@ -35,4 +35,4 @@ environment=prometheus_multiproc_dir=/tmp/prometheus_metrics
#### Accuracy
-If having long-term-accurate metrics in a multiprocess environment is important to you then it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using Netbox w/ `gunicorn` in a containerized enviroment following the 1-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [this issue](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
\ No newline at end of file
+If having accurate long-term metrics in a multiprocess environment is important to you then it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using Netbox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
\ No newline at end of file
From 2900013118021b48010e6b05595ee85a7f106f3c Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 17:24:25 -0400
Subject: [PATCH 22/43] Release v2.8.4
---
docs/release-notes/version-2.8.md | 2 +-
netbox/netbox/settings.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 17e04dad3..5d8687588 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,6 +1,6 @@
# NetBox v2.8
-v2.8.4 (FUTURE)
+v2.8.4 (2020-05-13)
### Enhancements
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 0162fabd0..f06a27980 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
-VERSION = '2.8.4-dev'
+VERSION = '2.8.4'
# Hostname
HOSTNAME = platform.node()
From 422eeddbef30369cb6630027d94162423e8c2c6f Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 13 May 2020 17:32:27 -0400
Subject: [PATCH 23/43] Post-release version bump
---
netbox/netbox/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index f06a27980..56fd9bb0f 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
-VERSION = '2.8.4'
+VERSION = '2.8.5-dev'
# Hostname
HOSTNAME = platform.node()
From 2c2d6c6d47b68a659a527bc2aeb45c01e1e97083 Mon Sep 17 00:00:00 2001
From: John Anderson
Date: Fri, 15 May 2020 02:31:45 -0400
Subject: [PATCH 24/43] fixes #3304 - primary IP address caching invalidation
---
docs/release-notes/version-2.8.md | 8 ++++++++
netbox/dcim/views.py | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 5d8687588..cb611f25f 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,5 +1,13 @@
# NetBox v2.8
+v2.8.5 (FUTURE)
+
+### Bug Fixes
+
+* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
+
+---
+
v2.8.4 (2020-05-13)
### Enhancements
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index cd1b4edf4..d141f93c6 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -1105,7 +1105,7 @@ class DeviceView(PermissionRequiredMixin, View):
def get(self, request, pk):
device = get_object_or_404(Device.objects.prefetch_related(
- 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
+ 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
), pk=pk)
# VirtualChassis members
From 14744da8f6f045dfe38b16ba40a6f1e96fe9b114 Mon Sep 17 00:00:00 2001
From: John Anderson
Date: Fri, 15 May 2020 02:45:48 -0400
Subject: [PATCH 25/43] fixes #4647 - caching invalidation related to assinging
new IP addresses to interfaces
---
docs/release-notes/version-2.8.md | 2 ++
netbox/ipam/forms.py | 7 ++++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index cb611f25f..ed5f01709 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -5,6 +5,8 @@ v2.8.5 (FUTURE)
### Bug Fixes
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
+* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assinging new IP addresses to interfaces
+
---
diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py
index f5fd6e5f8..5906e19a4 100644
--- a/netbox/ipam/forms.py
+++ b/netbox/ipam/forms.py
@@ -618,7 +618,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
if self.instance and self.instance.interface:
self.fields['interface'].queryset = Interface.objects.filter(
device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
- )
+ ).prefetch_related(
+ 'device__primary_ip4',
+ 'device__primary_ip6',
+ 'virtual_machine__primary_ip4',
+ 'virtual_machine__primary_ip6',
+ ) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save()
else:
self.fields['interface'].choices = []
From 8394ff55371d658012e769f03e3aeb22f308ba8d Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 15 May 2020 09:02:56 -0400
Subject: [PATCH 26/43] Fixes #4644: Fix ordering of services table by parent
---
docs/release-notes/version-2.8.md | 4 ++--
netbox/ipam/tables.py | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index ed5f01709..6165e6a15 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -5,8 +5,8 @@ v2.8.5 (FUTURE)
### Bug Fixes
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
-* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assinging new IP addresses to interfaces
-
+* [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
+* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
---
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index d8b50c11d..ca48c2951 100644
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -667,6 +667,9 @@ class ServiceTable(BaseTable):
viewname='ipam:service',
args=[Accessor('pk')]
)
+ parent = tables.LinkColumn(
+ order_by=('device', 'virtual_machine')
+ )
tags = TagColumn(
url_name='ipam:service_list'
)
From ba91b3aa2e1a85a481f163506409189187dd8921 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 15 May 2020 09:13:51 -0400
Subject: [PATCH 27/43] Fixes #4646: Correct UI link for reports with custom
name
---
docs/release-notes/version-2.8.md | 1 +
netbox/extras/reports.py | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 6165e6a15..32d20d700 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -6,6 +6,7 @@ v2.8.5 (FUTURE)
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
* [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
+* [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
---
diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py
index 373acdde7..d4db12daa 100644
--- a/netbox/extras/reports.py
+++ b/netbox/extras/reports.py
@@ -92,7 +92,7 @@ class Report(object):
self.active_test = None
self.failed = False
- self.logger = logging.getLogger(f"netbox.reports.{self.module}.{self.name}")
+ self.logger = logging.getLogger(f"netbox.reports.{self.full_name}")
# Compile test methods and initialize results skeleton
test_methods = []
@@ -120,7 +120,7 @@ class Report(object):
@property
def full_name(self):
- return '.'.join([self.module, self.name])
+ return '.'.join([self.__module__, self.__class__.__name__])
def _log(self, obj, message, level=LOG_DEFAULT):
"""
From a64351279ddc883a5b90838f4977b22f40ec3503 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 15 May 2020 09:36:16 -0400
Subject: [PATCH 28/43] Fixes #4648: Fix bulk CSV import of child devices
---
docs/release-notes/version-2.8.md | 1 +
netbox/dcim/forms.py | 16 +++++++++++++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 32d20d700..6d1907eb0 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -8,6 +8,7 @@ v2.8.5 (FUTURE)
* [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
* [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
+* [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
---
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index 5d3ec1019..cdd42ddae 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -1956,7 +1956,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
help_text='Parent device'
)
device_bay = CSVModelChoiceField(
- queryset=Device.objects.all(),
+ queryset=DeviceBay.objects.all(),
to_field_name='name',
help_text='Device bay in which this device is installed'
)
@@ -1976,6 +1976,20 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')}
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
+ def clean(self):
+ super().clean()
+
+ # Set parent_bay reverse relationship
+ device_bay = self.cleaned_data.get('device_bay')
+ if device_bay:
+ self.instance.parent_bay = device_bay
+
+ # Inherit site and rack from parent device
+ parent = self.cleaned_data.get('parent')
+ if parent:
+ self.instance.site = parent.site
+ self.instance.rack = parent.rack
+
class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
From 3c8e7e739d2fe4cbb7729a91d49e95f8b09c20b1 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 15 May 2020 09:44:00 -0400
Subject: [PATCH 29/43] Fixes #4649: Fix interface assignment for bulk-imported
IP addresses
---
docs/release-notes/version-2.8.md | 1 +
netbox/ipam/forms.py | 12 ------------
2 files changed, 1 insertion(+), 12 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 6d1907eb0..60adf53cc 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -9,6 +9,7 @@ v2.8.5 (FUTURE)
* [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
* [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
+* [#4649](https://github.com/netbox-community/netbox/issues/4649) - Fix interface assignment for bulk-imported IP addresses
---
diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py
index 5906e19a4..fc1352ec9 100644
--- a/netbox/ipam/forms.py
+++ b/netbox/ipam/forms.py
@@ -780,18 +780,6 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
def save(self, *args, **kwargs):
- # Set interface
- if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
- self.instance.interface = Interface.objects.get(
- device=self.cleaned_data['device'],
- name=self.cleaned_data['interface_name']
- )
- elif self.cleaned_data['virtual_machine'] and self.cleaned_data['interface_name']:
- self.instance.interface = Interface.objects.get(
- virtual_machine=self.cleaned_data['virtual_machine'],
- name=self.cleaned_data['interface_name']
- )
-
ipaddress = super().save(*args, **kwargs)
# Set as primary for device/VM
From cd236aa8862cf05e90e227c589cd0e054afc6e6b Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Fri, 15 May 2020 10:11:36 -0400
Subject: [PATCH 30/43] Closes #4645: Update minimum required version of
PostgreSQL to 9.6
---
docs/index.md | 2 +-
docs/installation/1-postgresql.md | 4 ++--
docs/release-notes/version-2.8.md | 2 ++
netbox/templates/exceptions/programming_error.html | 2 +-
4 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/docs/index.md b/docs/index.md
index 3880c9d07..ee7f77f69 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -49,7 +49,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| HTTP service | nginx or Apache |
| WSGI service | gunicorn or uWSGI |
| Application | Django/Python |
-| Database | PostgreSQL 9.4+ |
+| Database | PostgreSQL 9.6+ |
| Task queuing | Redis/django-rq |
| Live device access | NAPALM |
diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md
index afe3a51d2..933e32edc 100644
--- a/docs/installation/1-postgresql.md
+++ b/docs/installation/1-postgresql.md
@@ -3,7 +3,7 @@
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
!!! warning
- NetBox requires PostgreSQL 9.4 or higher. Please note that MySQL and other relational databases are **not** supported.
+ NetBox requires PostgreSQL 9.6 or higher. Please note that MySQL and other relational databases are **not** supported.
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
@@ -51,7 +51,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
```no-highlight
# sudo -u postgres psql
-psql (9.4.5)
+psql (10.10)
Type "help" for help.
postgres=# CREATE DATABASE netbox;
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 60adf53cc..9574894a3 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -2,6 +2,8 @@
v2.8.5 (FUTURE)
+**Note:** The minimum required version of PostgreSQL is now 9.6.
+
### Bug Fixes
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
diff --git a/netbox/templates/exceptions/programming_error.html b/netbox/templates/exceptions/programming_error.html
index 6f10c2e27..48ab707b7 100644
--- a/netbox/templates/exceptions/programming_error.html
+++ b/netbox/templates/exceptions/programming_error.html
@@ -10,7 +10,7 @@
python3 manage.py migrate from the command line.
- Unsupported PostgreSQL version - Ensure that PostgreSQL version 9.4 or higher is in use. You
+ Unsupported PostgreSQL version - Ensure that PostgreSQL version 9.6 or higher is in use. You
can check this by connecting to the database using NetBox's credentials and issuing a query for
SELECT VERSION().
From d2e1428c75ce787d33486e69b501f0fac3bda84d Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 20 May 2020 09:36:55 -0400
Subject: [PATCH 31/43] Closes #4665: Add NEMA L14 and L21 power port/outlet
types
---
docs/release-notes/version-2.8.md | 4 ++++
netbox/dcim/choices.py | 16 ++++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 9574894a3..ad86ca9df 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -4,6 +4,10 @@ v2.8.5 (FUTURE)
**Note:** The minimum required version of PostgreSQL is now 9.6.
+### Enhancements
+
+* [#4665](https://github.com/netbox-community/netbox/issues/4665) - Add NEMA L14 and L21 power port/outlet types
+
### Bug Fixes
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 8433bb152..479563093 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -276,6 +276,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEMA_L620P = 'nema-l6-20p'
TYPE_NEMA_L630P = 'nema-l6-30p'
TYPE_NEMA_L650P = 'nema-l6-50p'
+ TYPE_NEMA_L1420P = 'nema-l14-20p'
+ TYPE_NEMA_L1430P = 'nema-l14-30p'
+ TYPE_NEMA_L2120P = 'nema-l21-20p'
+ TYPE_NEMA_L2130P = 'nema-l21-30p'
# California style
TYPE_CS6361C = 'cs6361c'
TYPE_CS6365C = 'cs6365c'
@@ -337,6 +341,10 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_L620P, 'NEMA L6-20P'),
(TYPE_NEMA_L630P, 'NEMA L6-30P'),
(TYPE_NEMA_L650P, 'NEMA L6-50P'),
+ (TYPE_NEMA_L1420P, 'NEMA L14-20P'),
+ (TYPE_NEMA_L1430P, 'NEMA L14-30P'),
+ (TYPE_NEMA_L2120P, 'NEMA L21-20P'),
+ (TYPE_NEMA_L2130P, 'NEMA L21-30P'),
)),
('California Style', (
(TYPE_CS6361C, 'CS6361C'),
@@ -405,6 +413,10 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_NEMA_L620R = 'nema-l6-20r'
TYPE_NEMA_L630R = 'nema-l6-30r'
TYPE_NEMA_L650R = 'nema-l6-50r'
+ TYPE_NEMA_L1420R = 'nema-l14-20r'
+ TYPE_NEMA_L1430R = 'nema-l14-30r'
+ TYPE_NEMA_L2120R = 'nema-l21-20r'
+ TYPE_NEMA_L2130R = 'nema-l21-30r'
# California style
TYPE_CS6360C = 'CS6360C'
TYPE_CS6364C = 'CS6364C'
@@ -467,6 +479,10 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_L620R, 'NEMA L6-20R'),
(TYPE_NEMA_L630R, 'NEMA L6-30R'),
(TYPE_NEMA_L650R, 'NEMA L6-50R'),
+ (TYPE_NEMA_L1420R, 'NEMA L14-20R'),
+ (TYPE_NEMA_L1430R, 'NEMA L14-30R'),
+ (TYPE_NEMA_L2120R, 'NEMA L21-20R'),
+ (TYPE_NEMA_L2130R, 'NEMA L21-30R'),
)),
('California Style', (
(TYPE_CS6360C, 'CS6360C'),
From 1f5d2520c3f17be08497820e3b7d7904e80eaac9 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Wed, 20 May 2020 10:37:26 -0400
Subject: [PATCH 32/43] Formatting fix
---
docs/release-notes/version-2.8.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index ad86ca9df..16712bb79 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,6 +1,6 @@
# NetBox v2.8
-v2.8.5 (FUTURE)
+## v2.8.5 (FUTURE)
**Note:** The minimum required version of PostgreSQL is now 9.6.
@@ -19,7 +19,7 @@ v2.8.5 (FUTURE)
---
-v2.8.4 (2020-05-13)
+## v2.8.4 (2020-05-13)
### Enhancements
From 27700d316f7ae94522c073f4533822003288fd6c Mon Sep 17 00:00:00 2001
From: Sander Steffann
Date: Fri, 22 May 2020 22:24:05 +0200
Subject: [PATCH 33/43] Add `perms` to PluginTemplateExtension context
---
netbox/extras/templatetags/plugins.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/netbox/extras/templatetags/plugins.py b/netbox/extras/templatetags/plugins.py
index b66cce0a6..3f593fa10 100644
--- a/netbox/extras/templatetags/plugins.py
+++ b/netbox/extras/templatetags/plugins.py
@@ -18,6 +18,7 @@ def _get_registered_content(obj, method, template_context):
'object': obj,
'request': template_context['request'],
'settings': template_context['settings'],
+ 'perms': template_context['perms'],
}
model_name = obj._meta.label_lower
From ff3b348771c07802b13c6a813ef6e4aed4e0b46c Mon Sep 17 00:00:00 2001
From: Sander Steffann
Date: Fri, 22 May 2020 22:28:04 +0200
Subject: [PATCH 34/43] Add `csrf_token` to PluginTemplateExtension context
---
netbox/extras/templatetags/plugins.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/netbox/extras/templatetags/plugins.py b/netbox/extras/templatetags/plugins.py
index b66cce0a6..e63d25df1 100644
--- a/netbox/extras/templatetags/plugins.py
+++ b/netbox/extras/templatetags/plugins.py
@@ -18,6 +18,7 @@ def _get_registered_content(obj, method, template_context):
'object': obj,
'request': template_context['request'],
'settings': template_context['settings'],
+ 'csrf_token': template_context['csrf_token'],
}
model_name = obj._meta.label_lower
From 74c29b0bb72bd6ba9a2ba1e19f8121da1d0f043e Mon Sep 17 00:00:00 2001
From: kobayashi
Date: Tue, 26 May 2020 01:17:10 -0400
Subject: [PATCH 35/43] Fixes #4684: Fix ignored comment when importing
DeviceType
---
docs/release-notes/version-2.8.md | 1 +
netbox/dcim/forms.py | 1 +
netbox/dcim/tests/test_views.py | 2 ++
3 files changed, 4 insertions(+)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 16712bb79..5507d420e 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -16,6 +16,7 @@
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
* [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
* [#4649](https://github.com/netbox-community/netbox/issues/4649) - Fix interface assignment for bulk-imported IP addresses
+* [#4684](https://github.com/netbox-community/netbox/issues/4684) - Fix ignored comment field when adding device type via YAML/JSON import.
---
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index cdd42ddae..94cf51fcd 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -932,6 +932,7 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
+ 'comments',
]
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 65f37c1d5..7ee5d7845 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -366,6 +366,7 @@ manufacturer: Generic
model: TEST-1000
slug: test-1000
u_height: 2
+comments: test comment
console-ports:
- name: Console Port 1
type: de-9
@@ -456,6 +457,7 @@ device-bays:
self.assertHttpStatus(response, 200)
dt = DeviceType.objects.get(model='TEST-1000')
+ self.assertEqual(dt.comments, 'test comment')
# Verify all of the components were created
self.assertEqual(dt.consoleport_templates.count(), 3)
From 9cde377133af3ac0e61384757733afa4cc09f653 Mon Sep 17 00:00:00 2001
From: kobayashi
Date: Tue, 26 May 2020 01:23:23 -0400
Subject: [PATCH 36/43] Closes #4676: Set default value of
REMOTE_AUTH_AUTO_CREATE_USER as False in docs
---
docs/configuration/optional-settings.md | 2 +-
docs/release-notes/version-2.8.md | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md
index 4d5251f25..617878fbb 100644
--- a/docs/configuration/optional-settings.md
+++ b/docs/configuration/optional-settings.md
@@ -385,7 +385,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
## REMOTE_AUTH_AUTO_CREATE_USER
-Default: `True`
+Default: `False`
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 16712bb79..0827fc535 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -16,6 +16,7 @@
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
* [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
* [#4649](https://github.com/netbox-community/netbox/issues/4649) - Fix interface assignment for bulk-imported IP addresses
+* [#4676](https://github.com/netbox-community/netbox/issues/4676) - Set default value of `REMOTE_AUTH_AUTO_CREATE_USER` as `False` in docs
---
From a5785552d9d832029b288aa22e9d8e09164c955e Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 09:05:18 -0400
Subject: [PATCH 37/43] Changelog for #4651, #4652
---
docs/release-notes/version-2.8.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 16712bb79..9d3979398 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -6,6 +6,8 @@
### Enhancements
+* [#4651](https://github.com/netbox-community/netbox/issues/4651) - Add `csrf_token` context for plugin templates
+* [#4652](https://github.com/netbox-community/netbox/issues/4652) - Add permissions context for plugin templates
* [#4665](https://github.com/netbox-community/netbox/issues/4665) - Add NEMA L14 and L21 power port/outlet types
### Bug Fixes
From 92f49b471170f2c2c82d831694e9b7db8d6dd24f Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 09:36:27 -0400
Subject: [PATCH 38/43] Closes #4672: Set default color for rack and devices
roles
---
docs/release-notes/version-2.8.md | 1 +
netbox/dcim/filters.py | 4 +-
.../migrations/0106_role_default_color.py | 24 +++++++
netbox/dcim/models/__init__.py | 9 ++-
netbox/extras/models/tags.py | 3 +-
netbox/utilities/choices.py | 64 +++++++++++++++++++
netbox/utilities/constants.py | 31 ---------
netbox/utilities/forms.py | 5 +-
8 files changed, 102 insertions(+), 39 deletions(-)
create mode 100644 netbox/dcim/migrations/0106_role_default_color.py
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 720e4b1a7..d761020ad 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -9,6 +9,7 @@
* [#4651](https://github.com/netbox-community/netbox/issues/4651) - Add `csrf_token` context for plugin templates
* [#4652](https://github.com/netbox-community/netbox/issues/4652) - Add permissions context for plugin templates
* [#4665](https://github.com/netbox-community/netbox/issues/4665) - Add NEMA L14 and L21 power port/outlet types
+* [#4672](https://github.com/netbox-community/netbox/issues/4672) - Set default color for rack and devices roles
### Bug Fixes
diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py
index 5bc6dd7f0..8c24180bb 100644
--- a/netbox/dcim/filters.py
+++ b/netbox/dcim/filters.py
@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet
from tenancy.models import Tenant
-from utilities.constants import COLOR_CHOICES
+from utilities.choices import ColorChoices
from utilities.filters import (
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
@@ -1084,7 +1084,7 @@ class CableFilterSet(BaseFilterSet):
choices=CableStatusChoices
)
color = django_filters.MultipleChoiceFilter(
- choices=COLOR_CHOICES
+ choices=ColorChoices
)
device_id = MultiValueNumberFilter(
method='filter_device'
diff --git a/netbox/dcim/migrations/0106_role_default_color.py b/netbox/dcim/migrations/0106_role_default_color.py
new file mode 100644
index 000000000..c4df1b33f
--- /dev/null
+++ b/netbox/dcim/migrations/0106_role_default_color.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.6 on 2020-05-26 13:33
+
+from django.db import migrations
+import utilities.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0105_interface_name_collation'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='devicerole',
+ name='color',
+ field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
+ ),
+ migrations.AlterField(
+ model_name='rackrole',
+ name='color',
+ field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
+ ),
+ ]
diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py
index 490667153..1f6478119 100644
--- a/netbox/dcim/models/__init__.py
+++ b/netbox/dcim/models/__init__.py
@@ -23,6 +23,7 @@ from dcim.fields import ASNField
from dcim.elevations import RackElevationSVG
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features
+from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object, to_meters
@@ -379,7 +380,9 @@ class RackRole(ChangeLoggedModel):
slug = models.SlugField(
unique=True
)
- color = ColorField()
+ color = ColorField(
+ default=ColorChoices.COLOR_GREY
+ )
description = models.CharField(
max_length=200,
blank=True,
@@ -1190,7 +1193,9 @@ class DeviceRole(ChangeLoggedModel):
slug = models.SlugField(
unique=True
)
- color = ColorField()
+ color = ColorField(
+ default=ColorChoices.COLOR_GREY
+ )
vm_role = models.BooleanField(
default=True,
verbose_name='VM Role',
diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py
index 3bad7fa8b..d68ca2ce6 100644
--- a/netbox/extras/models/tags.py
+++ b/netbox/extras/models/tags.py
@@ -3,6 +3,7 @@ from django.urls import reverse
from django.utils.text import slugify
from taggit.models import TagBase, GenericTaggedItemBase
+from utilities.choices import ColorChoices
from utilities.fields import ColorField
from utilities.models import ChangeLoggedModel
@@ -13,7 +14,7 @@ from utilities.models import ChangeLoggedModel
class Tag(TagBase, ChangeLoggedModel):
color = ColorField(
- default='9e9e9e'
+ default=ColorChoices.COLOR_GREY
)
description = models.CharField(
max_length=200,
diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py
index aba64e63b..ce0929a8b 100644
--- a/netbox/utilities/choices.py
+++ b/netbox/utilities/choices.py
@@ -80,6 +80,70 @@ def unpack_grouped_choices(choices):
return unpacked_choices
+#
+# Generic color choices
+#
+
+class ColorChoices(ChoiceSet):
+ COLOR_DARK_RED = 'aa1409'
+ COLOR_RED = 'f44336'
+ COLOR_PINK = 'e91e63'
+ COLOR_ROSE = 'ffe4e1'
+ COLOR_FUCHSIA = 'ff66ff'
+ COLOR_PURPLE = '9c27b0'
+ COLOR_DARK_PURPLE = '673ab7'
+ COLOR_INDIGO = '3f51b5'
+ COLOR_BLUE = '2196f3'
+ COLOR_LIGHT_BLUE = '03a9f4'
+ COLOR_CYAN = '00bcd4'
+ COLOR_TEAL = '009688'
+ COLOR_AQUA = '00ffff'
+ COLOR_DARK_GREEN = '2f6a31'
+ COLOR_GREEN = '4caf50'
+ COLOR_LIGHT_GREEN = '8bc34a'
+ COLOR_LIME = 'cddc39'
+ COLOR_YELLOW = 'ffeb3b'
+ COLOR_AMBER = 'ffc107'
+ COLOR_ORANGE = 'ff9800'
+ COLOR_DARK_ORANGE = 'ff5722'
+ COLOR_BROWN = '795548'
+ COLOR_LIGHT_GREY = 'c0c0c0'
+ COLOR_GREY = '9e9e9e'
+ COLOR_DARK_GREY = '607d8b'
+ COLOR_BLACK = '111111'
+ COLOR_WHITE = 'ffffff'
+
+ CHOICES = (
+ (COLOR_DARK_RED, 'Dark red'),
+ (COLOR_RED, 'Red'),
+ (COLOR_PINK, 'Pink'),
+ (COLOR_ROSE, 'Rose'),
+ (COLOR_FUCHSIA, 'Fuchsia'),
+ (COLOR_PURPLE, 'Purple'),
+ (COLOR_DARK_PURPLE, 'Dark purple'),
+ (COLOR_INDIGO, 'Indigo'),
+ (COLOR_BLUE, 'Blue'),
+ (COLOR_LIGHT_BLUE, 'Light blue'),
+ (COLOR_CYAN, 'Cyan'),
+ (COLOR_TEAL, 'Teal'),
+ (COLOR_AQUA, 'Aqua'),
+ (COLOR_DARK_GREEN, 'Dark green'),
+ (COLOR_GREEN, 'Green'),
+ (COLOR_LIGHT_GREEN, 'Light green'),
+ (COLOR_LIME, 'Lime'),
+ (COLOR_YELLOW, 'Yellow'),
+ (COLOR_AMBER, 'Amber'),
+ (COLOR_ORANGE, 'Orange'),
+ (COLOR_DARK_ORANGE, 'Dark orange'),
+ (COLOR_BROWN, 'Brown'),
+ (COLOR_LIGHT_GREY, 'Light grey'),
+ (COLOR_GREY, 'Grey'),
+ (COLOR_DARK_GREY, 'Dark grey'),
+ (COLOR_BLACK, 'Black'),
+ (COLOR_WHITE, 'White'),
+ )
+
+
#
# Button color choices
#
diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py
index bdcdeef11..9a3a7d028 100644
--- a/netbox/utilities/constants.py
+++ b/netbox/utilities/constants.py
@@ -1,34 +1,3 @@
-COLOR_CHOICES = (
- ('aa1409', 'Dark red'),
- ('f44336', 'Red'),
- ('e91e63', 'Pink'),
- ('ffe4e1', 'Rose'),
- ('ff66ff', 'Fuschia'),
- ('9c27b0', 'Purple'),
- ('673ab7', 'Dark purple'),
- ('3f51b5', 'Indigo'),
- ('2196f3', 'Blue'),
- ('03a9f4', 'Light blue'),
- ('00bcd4', 'Cyan'),
- ('009688', 'Teal'),
- ('00ffff', 'Aqua'),
- ('2f6a31', 'Dark green'),
- ('4caf50', 'Green'),
- ('8bc34a', 'Light green'),
- ('cddc39', 'Lime'),
- ('ffeb3b', 'Yellow'),
- ('ffc107', 'Amber'),
- ('ff9800', 'Orange'),
- ('ff5722', 'Dark orange'),
- ('795548', 'Brown'),
- ('c0c0c0', 'Light grey'),
- ('9e9e9e', 'Grey'),
- ('607d8b', 'Dark grey'),
- ('111111', 'Black'),
- ('ffffff', 'White'),
-)
-
-
#
# Filter lookup expressions
#
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
index bfc783631..17ef4dd84 100644
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -14,8 +14,7 @@ from django.forms import BoundField
from django.forms.models import fields_for_model
from django.urls import reverse
-from .choices import unpack_grouped_choices
-from .constants import *
+from .choices import ColorChoices, unpack_grouped_choices
from .validators import EnhancedURLValidator
NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
@@ -163,7 +162,7 @@ class ColorSelect(forms.Select):
option_template_name = 'widgets/colorselect_option.html'
def __init__(self, *args, **kwargs):
- kwargs['choices'] = add_blank_choice(COLOR_CHOICES)
+ kwargs['choices'] = add_blank_choice(ColorChoices)
super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-select2-color-picker'
From 88cffca2705dc3774680a1bc11441491df0d5e3e Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 10:01:49 -0400
Subject: [PATCH 39/43] Closes #4650: Expose INTERNAL_IPS configuration
parameter
---
docs/configuration/optional-settings.md | 17 ++++++++++++++++-
docs/release-notes/version-2.8.md | 1 +
netbox/netbox/configuration.example.py | 4 ++++
netbox/netbox/settings.py | 10 +---------
4 files changed, 22 insertions(+), 10 deletions(-)
diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md
index 617878fbb..3c4392915 100644
--- a/docs/configuration/optional-settings.md
+++ b/docs/configuration/optional-settings.md
@@ -86,7 +86,12 @@ CORS_ORIGIN_WHITELIST = [
Default: False
-This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
+This setting enables debugging. This should be done only during development or troubleshooting. Note that only clients
+which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
+interface.
+
+!!! warning
+ Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
---
@@ -184,6 +189,16 @@ HTTP_PROXIES = {
---
+## INTERNAL_IPS
+
+Default: `('127.0.0.1', '::1',)`
+
+A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
+example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
+addresses (and [`DEBUG`](#debug) is true).
+
+---
+
## LOGGING
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index d761020ad..f28f8af7d 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -6,6 +6,7 @@
### Enhancements
+* [#4650](https://github.com/netbox-community/netbox/issues/4650) - Expose `INTERNAL_IPS` configuration parameter
* [#4651](https://github.com/netbox-community/netbox/issues/4651) - Add `csrf_token` context for plugin templates
* [#4652](https://github.com/netbox-community/netbox/issues/4652) - Add permissions context for plugin templates
* [#4665](https://github.com/netbox-community/netbox/issues/4665) - Add NEMA L14 and L21 power port/outlet types
diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py
index a020c4322..941cbcd88 100644
--- a/netbox/netbox/configuration.example.py
+++ b/netbox/netbox/configuration.example.py
@@ -132,6 +132,10 @@ EXEMPT_VIEW_PERMISSIONS = [
# 'https': 'http://10.10.1.10:1080',
# }
+# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing
+# NetBox from an internal IP.
+INTERNAL_IPS = ('127.0.0.1', '::1')
+
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/stable/topics/logging/
LOGGING = {}
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 56fd9bb0f..b1978d749 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -78,6 +78,7 @@ EMAIL = getattr(configuration, 'EMAIL', {})
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
+INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
@@ -615,15 +616,6 @@ RQ_QUEUES = {
'check_releases': RQ_PARAMS,
}
-#
-# Django debug toolbar
-#
-
-INTERNAL_IPS = (
- '127.0.0.1',
- '::1',
-)
-
#
# NetBox internal settings
From e54d44143324067e92460958a8228f75db8cb459 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 10:06:46 -0400
Subject: [PATCH 40/43] Remove "disable plugins" from bug report to prevent
irrelevant search results
---
.github/ISSUE_TEMPLATE/bug_report.md | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index e1012212d..54dc5ca8c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -30,10 +30,9 @@ about: Report a reproducible bug in the current release of NetBox
library such as pynetbox.
-->
### Steps to Reproduce
-1. Disable any installed plugins by commenting out the `PLUGINS` setting in
- `configuration.py`.
-2.
-3.
+1.
+2.
+3.
### Expected Behavior
From ccc31b2c7c8a5cdae988af2f569b5b49a1d7a059 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 15:34:29 -0400
Subject: [PATCH 41/43] Fixes #4525: Allow passing initial data to custom
script MultiObjectVar
---
docs/release-notes/version-2.8.md | 1 +
netbox/extras/forms.py | 6 +++---
netbox/utilities/forms.py | 15 ++++++++++++---
3 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index f28f8af7d..9bae04896 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -15,6 +15,7 @@
### Bug Fixes
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
+* [#4525](https://github.com/netbox-community/netbox/issues/4525) - Allow passing initial data to custom script MultiObjectVar
* [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
* [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py
index 469b55efd..cb9930ae2 100644
--- a/netbox/extras/forms.py
+++ b/netbox/extras/forms.py
@@ -432,11 +432,11 @@ class ScriptForm(BootstrapMixin, forms.Form):
def __init__(self, vars, *args, commit_default=True, **kwargs):
- super().__init__(*args, **kwargs)
-
# Dynamically populate fields for variables
for name, var in vars.items():
- self.fields[name] = var.as_field()
+ self.base_fields[name] = var.as_field()
+
+ super().__init__(*args, **kwargs)
# Toggle default commit behavior based on Meta option
if not commit_default:
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
index 17ef4dd84..979b6ac32 100644
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -606,15 +606,18 @@ class DynamicModelChoiceMixin:
filter = django_filters.ModelChoiceFilter
widget = APISelect
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
+ def _get_initial_value(self, initial_data, field_name):
+ return initial_data.get(field_name)
def get_bound_field(self, form, field_name):
bound_field = BoundField(form, self, field_name)
+ # Override initial() to allow passing multiple values
+ bound_field.initial = self._get_initial_value(form.initial, field_name)
+
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
# will be populated on-demand via the APISelect widget.
- data = self.prepare_value(bound_field.data or bound_field.initial)
+ data = bound_field.value()
if data:
filter = self.filter(field_name=self.to_field_name or 'pk', queryset=self.queryset)
self.queryset = filter.filter(self.queryset, data)
@@ -647,6 +650,12 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
filter = django_filters.ModelMultipleChoiceFilter
widget = APISelectMultiple
+ def _get_initial_value(self, initial_data, field_name):
+ # If a QueryDict has been passed as initial form data, get *all* listed values
+ if hasattr(initial_data, 'getlist'):
+ return initial_data.getlist(field_name)
+ return initial_data.get(field_name)
+
class LaxURLField(forms.URLField):
"""
From c9a7527f33dfdfb62190e3c41a56b323f95bb95f Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 16:17:01 -0400
Subject: [PATCH 42/43] Release v2.8.5
---
docs/release-notes/version-2.8.md | 2 +-
netbox/netbox/settings.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 9bae04896..5ca86217a 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,6 +1,6 @@
# NetBox v2.8
-## v2.8.5 (FUTURE)
+## v2.8.5 (2020-05-26)
**Note:** The minimum required version of PostgreSQL is now 9.6.
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index b1978d749..3b4971ce1 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
-VERSION = '2.8.5-dev'
+VERSION = '2.8.5'
# Hostname
HOSTNAME = platform.node()
From 56b7ab17340f880fc67b3c0893dba6dc5c4e71f2 Mon Sep 17 00:00:00 2001
From: Jeremy Stretch
Date: Tue, 26 May 2020 16:30:36 -0400
Subject: [PATCH 43/43] Post-release version bump
---
netbox/netbox/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 3b4971ce1..92c4a0cad 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
-VERSION = '2.8.5'
+VERSION = '2.8.6-dev'
# Hostname
HOSTNAME = platform.node()