mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge v3.1.8
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.1.7
|
placeholder: v3.1.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.1.7
|
placeholder: v3.1.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -11,10 +11,6 @@ The following sections detail how to set up a new instance of NetBox:
|
|||||||
5. [HTTP server](5-http-server.md)
|
5. [HTTP server](5-http-server.md)
|
||||||
6. [LDAP authentication](6-ldap.md) (optional)
|
6. [LDAP authentication](6-ldap.md) (optional)
|
||||||
|
|
||||||
The video below demonstrates the installation of NetBox v3.0 on Ubuntu 20.04 for your reference.
|
|
||||||
|
|
||||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/7Fpd2-q9_28" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| Dependency | Minimum Version |
|
| Dependency | Minimum Version |
|
||||||
|
@ -1,20 +1,30 @@
|
|||||||
# NetBox v3.1
|
# NetBox v3.1
|
||||||
|
|
||||||
## v3.1.8 (FUTURE)
|
## v3.1.9 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.1.8 (2022-02-15)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation
|
* [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation
|
||||||
* [#8398](https://github.com/netbox-community/netbox/issues/8398) - Embiggen configuration form fields for banner message content
|
* [#8398](https://github.com/netbox-community/netbox/issues/8398) - Embiggen configuration form fields for banner message content
|
||||||
|
* [#8556](https://github.com/netbox-community/netbox/issues/8556) - Add full username column to changelog table
|
||||||
|
* [#8620](https://github.com/netbox-community/netbox/issues/8620) - Enable tab completion for `nbshell`
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility
|
* [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility
|
||||||
|
* [#8391](https://github.com/netbox-community/netbox/issues/8391) - Null date columns should return empty strings during CSV export
|
||||||
* [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero
|
* [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero
|
||||||
* [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port
|
* [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port
|
||||||
* [#8564](https://github.com/netbox-community/netbox/issues/8564) - Fix errant table configuration key `available_columns`
|
* [#8564](https://github.com/netbox-community/netbox/issues/8564) - Fix errant table configuration key `available_columns`
|
||||||
|
* [#8577](https://github.com/netbox-community/netbox/issues/8577) - Show contact assignment counts in global search results
|
||||||
* [#8578](https://github.com/netbox-community/netbox/issues/8578) - Object change log tables should honor user's configured preferences
|
* [#8578](https://github.com/netbox-community/netbox/issues/8578) - Object change log tables should honor user's configured preferences
|
||||||
* [#8604](https://github.com/netbox-community/netbox/issues/8604) - Fix tag filter on config context list filter form
|
* [#8604](https://github.com/netbox-community/netbox/issues/8604) - Fix tag filter on config context list filter form
|
||||||
|
* [#8609](https://github.com/netbox-community/netbox/issues/8609) - Display validation error when attempting to assign VLANs to interface with no mode during bulk edit
|
||||||
|
* [#8611](https://github.com/netbox-community/netbox/issues/8611) - Fix bulk editing for certain custom link, webhook, and journal entry fields
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1114,8 +1114,14 @@ class InterfaceBulkEditForm(
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
if not self.cleaned_data['mode']:
|
||||||
|
if self.cleaned_data['untagged_vlan']:
|
||||||
|
raise forms.ValidationError({'untagged_vlan': "Interface mode must be specified to assign VLANs"})
|
||||||
|
elif self.cleaned_data['tagged_vlans']:
|
||||||
|
raise forms.ValidationError({'tagged_vlans': "Interface mode must be specified to assign VLANs"})
|
||||||
|
|
||||||
# Untagged interfaces cannot be assigned tagged VLANs
|
# Untagged interfaces cannot be assigned tagged VLANs
|
||||||
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
|
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
|
||||||
raise forms.ValidationError({
|
raise forms.ValidationError({
|
||||||
'mode': "An access interface cannot have tagged VLANs assigned."
|
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||||
})
|
})
|
||||||
|
@ -4,7 +4,9 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from utilities.forms import BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect
|
from utilities.forms import (
|
||||||
|
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextBulkEditForm',
|
'ConfigContextBulkEditForm',
|
||||||
@ -58,7 +60,7 @@ class CustomLinkBulkEditForm(BulkEditForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
button_class = forms.ChoiceField(
|
button_class = forms.ChoiceField(
|
||||||
choices=CustomLinkButtonClassChoices,
|
choices=add_blank_choice(CustomLinkButtonClassChoices),
|
||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
@ -116,21 +118,25 @@ class WebhookBulkEditForm(BulkEditForm):
|
|||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
)
|
)
|
||||||
http_method = forms.ChoiceField(
|
http_method = forms.ChoiceField(
|
||||||
choices=WebhookHttpMethodChoices,
|
choices=add_blank_choice(WebhookHttpMethodChoices),
|
||||||
required=False
|
required=False,
|
||||||
|
label='HTTP method'
|
||||||
)
|
)
|
||||||
payload_url = forms.CharField(
|
payload_url = forms.CharField(
|
||||||
required=False
|
required=False,
|
||||||
|
label='Payload URL'
|
||||||
)
|
)
|
||||||
ssl_verification = forms.NullBooleanField(
|
ssl_verification = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect(),
|
||||||
|
label='SSL verification'
|
||||||
)
|
)
|
||||||
secret = forms.CharField(
|
secret = forms.CharField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
ca_file_path = forms.CharField(
|
ca_file_path = forms.CharField(
|
||||||
required=False
|
required=False,
|
||||||
|
label='CA file path'
|
||||||
)
|
)
|
||||||
|
|
||||||
nullable_fields = ('secret', 'conditions', 'ca_file_path')
|
nullable_fields = ('secret', 'conditions', 'ca_file_path')
|
||||||
@ -179,7 +185,7 @@ class JournalEntryBulkEditForm(BulkEditForm):
|
|||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
kind = forms.ChoiceField(
|
kind = forms.ChoiceField(
|
||||||
choices=JournalEntryKindChoices,
|
choices=add_blank_choice(JournalEntryKindChoices),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
comments = forms.CharField(
|
comments = forms.CharField(
|
||||||
|
@ -70,10 +70,23 @@ class Command(BaseCommand):
|
|||||||
return namespace
|
return namespace
|
||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
|
namespace = self.get_namespace()
|
||||||
|
|
||||||
# If Python code has been passed, execute it and exit.
|
# If Python code has been passed, execute it and exit.
|
||||||
if options['command']:
|
if options['command']:
|
||||||
exec(options['command'], self.get_namespace())
|
exec(options['command'], namespace)
|
||||||
return
|
return
|
||||||
|
|
||||||
shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace())
|
# Try to enable tab-complete
|
||||||
|
try:
|
||||||
|
import readline
|
||||||
|
import rlcompleter
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
readline.set_completer(rlcompleter.Completer(namespace).complete)
|
||||||
|
readline.parse_and_bind('tab: complete')
|
||||||
|
|
||||||
|
# Run interactive shell
|
||||||
|
shell = code.interact(banner=BANNER_TEXT, local=namespace)
|
||||||
return shell
|
return shell
|
||||||
|
@ -26,6 +26,11 @@ CONFIGCONTEXT_ACTIONS = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
OBJECTCHANGE_FULL_NAME = """
|
||||||
|
{% load helpers %}
|
||||||
|
{{ record.user.get_full_name|placeholder }}
|
||||||
|
"""
|
||||||
|
|
||||||
OBJECTCHANGE_OBJECT = """
|
OBJECTCHANGE_OBJECT = """
|
||||||
{% if record.changed_object and record.changed_object.get_absolute_url %}
|
{% if record.changed_object and record.changed_object.get_absolute_url %}
|
||||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||||
@ -196,6 +201,14 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
format=settings.SHORT_DATETIME_FORMAT
|
format=settings.SHORT_DATETIME_FORMAT
|
||||||
)
|
)
|
||||||
|
user_name = tables.Column(
|
||||||
|
verbose_name='Username'
|
||||||
|
)
|
||||||
|
full_name = tables.TemplateColumn(
|
||||||
|
template_code=OBJECTCHANGE_FULL_NAME,
|
||||||
|
verbose_name='Full Name',
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
action = columns.ChoiceFieldColumn()
|
action = columns.ChoiceFieldColumn()
|
||||||
changed_object_type = columns.ContentTypeColumn(
|
changed_object_type = columns.ContentTypeColumn(
|
||||||
verbose_name='Type'
|
verbose_name='Type'
|
||||||
@ -212,7 +225,7 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
fields = ('id', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||||
|
|
||||||
|
|
||||||
class ObjectJournalTable(NetBoxTable):
|
class ObjectJournalTable(NetBoxTable):
|
||||||
|
@ -18,7 +18,7 @@ from ipam.filtersets import (
|
|||||||
from ipam.models import Aggregate, ASN, IPAddress, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, ASN, IPAddress, Prefix, VLAN, VRF
|
||||||
from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||||
from tenancy.filtersets import ContactFilterSet, TenantFilterSet
|
from tenancy.filtersets import ContactFilterSet, TenantFilterSet
|
||||||
from tenancy.models import Contact, Tenant
|
from tenancy.models import Contact, Tenant, ContactAssignment
|
||||||
from tenancy.tables import ContactTable, TenantTable
|
from tenancy.tables import ContactTable, TenantTable
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
|
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
|
||||||
@ -186,7 +186,7 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
'url': 'tenancy:tenant_list',
|
'url': 'tenancy:tenant_list',
|
||||||
}),
|
}),
|
||||||
('contact', {
|
('contact', {
|
||||||
'queryset': Contact.objects.prefetch_related('group', 'assignments'),
|
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')),
|
||||||
'filterset': ContactFilterSet,
|
'filterset': ContactFilterSet,
|
||||||
'table': ContactTable,
|
'table': ContactTable,
|
||||||
'url': 'tenancy:contact_list',
|
'url': 'tenancy:contact_list',
|
||||||
|
@ -4,9 +4,12 @@ from typing import Optional
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.db.models import DateField, DateTimeField
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.formats import date_format
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django_tables2.columns import library
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
@ -32,6 +35,42 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@library.register
|
||||||
|
class DateColumn(tables.DateColumn):
|
||||||
|
"""
|
||||||
|
Overrides the default implementation of DateColumn to better handle null values, returning a default value for
|
||||||
|
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
||||||
|
default, making this behavior consistent in all fields of type DateField.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def value(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_field(cls, field, **kwargs):
|
||||||
|
if isinstance(field, DateField):
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@library.register
|
||||||
|
class DateTimeColumn(tables.DateTimeColumn):
|
||||||
|
"""
|
||||||
|
Overrides the default implementation of DateTimeColumn to better handle null values, returning a default value for
|
||||||
|
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
||||||
|
default, making this behavior consistent in all fields of type DateTimeField.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def value(self, value):
|
||||||
|
if value:
|
||||||
|
return date_format(value, format="SHORT_DATETIME_FORMAT")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_field(cls, field, **kwargs):
|
||||||
|
if isinstance(field, DateTimeField):
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ToggleColumn(tables.CheckBoxColumn):
|
class ToggleColumn(tables.CheckBoxColumn):
|
||||||
"""
|
"""
|
||||||
Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
|
Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
|
||||||
|
@ -38,7 +38,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">User</th>
|
<th scope="row">User</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.user|default:object.user_name }}
|
{% if object.user.get_full_name %}
|
||||||
|
{{ object.user.get_full_name }} ({{ object.user_name }})
|
||||||
|
{% else %}
|
||||||
|
{{ object.user_name }}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -204,7 +204,7 @@ def deepmerge(original, new):
|
|||||||
"""
|
"""
|
||||||
merged = OrderedDict(original)
|
merged = OrderedDict(original)
|
||||||
for key, val in new.items():
|
for key, val in new.items():
|
||||||
if key in original and isinstance(original[key], dict) and isinstance(val, dict):
|
if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
|
||||||
merged[key] = deepmerge(original[key], val)
|
merged[key] = deepmerge(original[key], val)
|
||||||
else:
|
else:
|
||||||
merged[key] = val
|
merged[key] = val
|
||||||
|
@ -65,7 +65,7 @@ class ClusterCSVForm(NetBoxModelCSVForm):
|
|||||||
class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
||||||
status = CSVChoiceField(
|
status = CSVChoiceField(
|
||||||
choices=VirtualMachineStatusChoices,
|
choices=VirtualMachineStatusChoices,
|
||||||
help_text='Operational status of device'
|
help_text='Operational status'
|
||||||
)
|
)
|
||||||
cluster = CSVModelChoiceField(
|
cluster = CSVModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
|
@ -20,10 +20,10 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.0.3
|
Jinja2==3.0.3
|
||||||
Markdown==3.3.6
|
Markdown==3.3.6
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==8.1.9
|
mkdocs-material==8.1.11
|
||||||
mkdocstrings==0.17.0
|
mkdocstrings==0.17.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==8.4.0
|
Pillow==9.0.1
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
|
Reference in New Issue
Block a user