Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ICS Control System Infrastructure
Netbox
Commits
85c60670
Unverified
Commit
85c60670
authored
Nov 30, 2022
by
Jeremy Stretch
Committed by
GitHub
Nov 30, 2022
Browse files
Merge pull request #11059 from netbox-community/develop
Release v3.3.9
parents
bfda5d90
f2f36c67
Changes
32
Hide whitespace changes
Inline
Side-by-side
.github/ISSUE_TEMPLATE/bug_report.yaml
View file @
85c60670
...
...
@@ -14,7 +14,7 @@ body:
attributes
:
label
:
NetBox version
description
:
What version of NetBox are you currently running?
placeholder
:
v3.3.
8
placeholder
:
v3.3.
9
validations
:
required
:
true
-
type
:
dropdown
...
...
.github/ISSUE_TEMPLATE/feature_request.yaml
View file @
85c60670
...
...
@@ -14,7 +14,7 @@ body:
attributes
:
label
:
NetBox version
description
:
What version of NetBox are you currently running?
placeholder
:
v3.3.
8
placeholder
:
v3.3.
9
validations
:
required
:
true
-
type
:
dropdown
...
...
docs/customization/reports.md
View file @
85c60670
...
...
@@ -45,7 +45,7 @@ class DeviceConnectionsReport(Report):
# Check that every console port for every active device has a connection defined.
active = DeviceStatusChoices.STATUS_ACTIVE
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
if console_port.connected_endpoint
is None
:
if
not
console_port.connected_endpoint
s
:
self.log_failure(
console_port.device,
"No console connection defined for {}".format(console_port.name)
...
...
@@ -64,7 +64,7 @@ class DeviceConnectionsReport(Report):
for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
connected_ports = 0
for power_port in PowerPort.objects.filter(device=device):
if power_port.connected_endpoint
is not None
:
if power_port.connected_endpoint
s
:
connected_ports += 1
if not power_port.path.is_active:
self.log_warning(
...
...
docs/release-notes/version-3.3.md
View file @
85c60670
# NetBox v3.3
## v3.3.9 (2022-11-30)
### Enhancements
*
[
#10653
](
https://github.com/netbox-community/netbox/issues/10653
)
- Ensure logging of failed login attempts
### Bug Fixes
*
[
#6389
](
https://github.com/netbox-community/netbox/issues/6389
)
- Call
`snapshot()`
on object when processing deletions
*
[
#9223
](
https://github.com/netbox-community/netbox/issues/9223
)
- Fix serialization of array field values in change log
*
[
#9878
](
https://github.com/netbox-community/netbox/issues/9878
)
- Fix spurious error message when rendering REST API docs
*
[
#10236
](
https://github.com/netbox-community/netbox/issues/10236
)
- Fix TypeError exception when viewing PDU configured for three-phase power
*
[
#10241
](
https://github.com/netbox-community/netbox/issues/10241
)
- Support referencing custom field related objects by attribute in addition to PK
*
[
#10579
](
https://github.com/netbox-community/netbox/issues/10579
)
- Mark cable traces terminating to a provider network as complete
*
[
#10721
](
https://github.com/netbox-community/netbox/issues/10721
)
- Disable ordering by custom object field columns
*
[
#10929
](
https://github.com/netbox-community/netbox/issues/10929
)
- Raise validation error when attempting to create a duplicate cable termination
*
[
#10936
](
https://github.com/netbox-community/netbox/issues/10936
)
- Permit demotion of device/VM primary IP via IP address edit form
*
[
#10938
](
https://github.com/netbox-community/netbox/issues/10938
)
-
`render_field`
template tag should respect
`label`
kwarg
*
[
#10969
](
https://github.com/netbox-community/netbox/issues/10969
)
- Update cable paths ending at associated rear port when creating new front ports
*
[
#10996
](
https://github.com/netbox-community/netbox/issues/10996
)
- Hide checkboxes on child object lists when no bulk operations are available
*
[
#10997
](
https://github.com/netbox-community/netbox/issues/10997
)
- Fix exception when editing NAT IP for VM with no cluster
*
[
#11014
](
https://github.com/netbox-community/netbox/issues/11014
)
- Use natural ordering when sorting rack elevations by name
*
[
#11028
](
https://github.com/netbox-community/netbox/issues/11028
)
- Enable bulk clearing of color attribute of pass-through ports
*
[
#11047
](
https://github.com/netbox-community/netbox/issues/11047
)
- Cloning a rack reservation should replicate rack & user
---
## v3.3.8 (2022-11-16)
### Enhancements
...
...
netbox/dcim/forms/bulk_edit.py
View file @
85c60670
...
...
@@ -1218,7 +1218,7 @@ class FrontPortBulkEditForm(
fieldsets
=
(
(
None
,
(
'module'
,
'type'
,
'label'
,
'color'
,
'description'
,
'mark_connected'
)),
)
nullable_fields
=
(
'module'
,
'label'
,
'description'
)
nullable_fields
=
(
'module'
,
'label'
,
'description'
,
'color'
)
class
RearPortBulkEditForm
(
...
...
@@ -1229,7 +1229,7 @@ class RearPortBulkEditForm(
fieldsets
=
(
(
None
,
(
'module'
,
'type'
,
'label'
,
'color'
,
'description'
,
'mark_connected'
)),
)
nullable_fields
=
(
'module'
,
'label'
,
'description'
)
nullable_fields
=
(
'module'
,
'label'
,
'description'
,
'color'
)
class
ModuleBayBulkEditForm
(
...
...
netbox/dcim/models/cables.py
View file @
85c60670
...
...
@@ -279,6 +279,17 @@ class CableTermination(models.Model):
def
clean
(
self
):
super
().
clean
()
# Check for existing termination
existing_termination
=
CableTermination
.
objects
.
exclude
(
cable
=
self
.
cable
).
filter
(
termination_type
=
self
.
termination_type
,
termination_id
=
self
.
termination_id
).
first
()
if
existing_termination
is
not
None
:
raise
ValidationError
(
f
"Duplicate termination found for
{
self
.
termination_type
.
app_label
}
.
{
self
.
termination_type
.
model
}
"
f
"
{
self
.
termination_id
}
: cable
{
existing_termination
.
cable
.
pk
}
"
)
# Validate interface type (if applicable)
if
self
.
termination_type
.
model
==
'interface'
and
self
.
termination
.
type
in
NONCONNECTABLE_IFACE_TYPES
:
raise
ValidationError
(
f
"Cables cannot be terminated to
{
self
.
termination
.
get_type_display
()
}
interfaces"
)
...
...
@@ -570,6 +581,7 @@ class CablePath(models.Model):
[
object_to_path_node
(
circuit_termination
)],
[
object_to_path_node
(
circuit_termination
.
provider_network
)],
])
is_complete
=
True
break
elif
circuit_termination
.
site
and
not
circuit_termination
.
cable
:
# Circuit terminates to a Site
...
...
netbox/dcim/models/device_components.py
View file @
85c60670
...
...
@@ -189,7 +189,7 @@ class PathEndpoint(models.Model):
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
CablePath model. `_path` should not be accessed directly; rather, use the `path` property.
`connected_endpoint()` is a convenience method for returning the destination of the associated CablePath, if any.
`connected_endpoint
s
()` is a convenience method for returning the destination of the associated CablePath, if any.
"""
_path
=
models
.
ForeignKey
(
to
=
'dcim.CablePath'
,
...
...
netbox/dcim/models/racks.py
View file @
85c60670
...
...
@@ -477,6 +477,8 @@ class RackReservation(NetBoxModel):
max_length
=
200
)
clone_fields
=
(
'rack'
,
'user'
,
'tenant'
)
class
Meta
:
ordering
=
[
'created'
,
'pk'
]
...
...
netbox/dcim/signals.py
View file @
85c60670
...
...
@@ -4,7 +4,9 @@ from django.db.models.signals import post_save, post_delete, pre_delete
from
django.dispatch
import
receiver
from
.choices
import
CableEndChoices
,
LinkStatusChoices
from
.models
import
Cable
,
CablePath
,
CableTermination
,
Device
,
PathEndpoint
,
PowerPanel
,
Rack
,
Location
,
VirtualChassis
from
.models
import
(
Cable
,
CablePath
,
CableTermination
,
Device
,
FrontPort
,
PathEndpoint
,
PowerPanel
,
Rack
,
Location
,
VirtualChassis
,
)
from
.models.cables
import
trace_paths
from
.utils
import
create_cablepath
,
rebuild_paths
...
...
@@ -123,3 +125,14 @@ def nullify_connected_endpoints(instance, **kwargs):
for
cablepath
in
CablePath
.
objects
.
filter
(
_nodes__contains
=
instance
.
cable
):
cablepath
.
retrace
()
@
receiver
(
post_save
,
sender
=
FrontPort
)
def
extend_rearport_cable_paths
(
instance
,
created
,
raw
,
**
kwargs
):
"""
When a new FrontPort is created, add it to any CablePaths which end at its corresponding RearPort.
"""
if
created
and
not
raw
:
rearport
=
instance
.
rear_port
for
cablepath
in
CablePath
.
objects
.
filter
(
_nodes__contains
=
rearport
):
cablepath
.
retrace
()
netbox/dcim/tests/test_cablepaths.py
View file @
85c60670
...
...
@@ -1323,6 +1323,7 @@ class CablePathTestCase(TestCase):
is_active
=
True
)
self
.
assertEqual
(
CablePath
.
objects
.
count
(),
1
)
self
.
assertTrue
(
CablePath
.
objects
.
first
().
is_complete
)
# Delete cable 1
cable1
.
delete
()
...
...
netbox/dcim/views.py
View file @
85c60670
...
...
@@ -589,17 +589,18 @@ class RackElevationListView(generic.ObjectListView):
racks
=
filtersets
.
RackFilterSet
(
request
.
GET
,
self
.
queryset
).
qs
total_count
=
racks
.
count
()
# Ordering
ORDERING_CHOICES
=
{
'name'
:
'Name (A-Z)'
,
'-name'
:
'Name (Z-A)'
,
'facility_id'
:
'Facility ID (A-Z)'
,
'-facility_id'
:
'Facility ID (Z-A)'
,
}
sort
=
request
.
GET
.
get
(
'sort'
,
"
name
"
)
sort
=
request
.
GET
.
get
(
'sort'
,
'
name
'
)
if
sort
not
in
ORDERING_CHOICES
:
sort
=
'name'
racks
=
racks
.
order_by
(
sort
)
sort_field
=
sort
.
replace
(
"name"
,
"_name"
)
# Use natural ordering
racks
=
racks
.
order_by
(
sort
_field
)
# Pagination
per_page
=
get_paginate_count
(
request
)
...
...
netbox/extras/api/customfields.py
View file @
85c60670
...
...
@@ -5,6 +5,7 @@ from rest_framework.serializers import ValidationError
from
extras.choices
import
CustomFieldTypeChoices
from
extras.models
import
CustomField
from
netbox.constants
import
NESTED_SERIALIZER_PREFIX
from
utilities.api
import
get_serializer_for_model
#
...
...
@@ -69,6 +70,23 @@ class CustomFieldsDataField(Field):
"values."
)
# Serialize object and multi-object values
for
cf
in
self
.
_get_custom_fields
():
if
cf
.
name
in
data
and
cf
.
type
in
(
CustomFieldTypeChoices
.
TYPE_OBJECT
,
CustomFieldTypeChoices
.
TYPE_MULTIOBJECT
):
serializer_class
=
get_serializer_for_model
(
model
=
cf
.
object_type
.
model_class
(),
prefix
=
NESTED_SERIALIZER_PREFIX
)
many
=
cf
.
type
==
CustomFieldTypeChoices
.
TYPE_MULTIOBJECT
serializer
=
serializer_class
(
data
=
data
[
cf
.
name
],
many
=
many
,
context
=
self
.
parent
.
context
)
if
serializer
.
is_valid
():
data
[
cf
.
name
]
=
[
obj
[
'id'
]
for
obj
in
serializer
.
data
]
if
many
else
serializer
.
data
[
'id'
]
else
:
raise
ValidationError
(
f
"Unknown related object(s):
{
data
[
cf
.
name
]
}
"
)
# If updating an existing instance, start with existing custom_field_data
if
self
.
parent
.
instance
:
data
=
{
**
self
.
parent
.
instance
.
custom_field_data
,
**
data
}
...
...
netbox/extras/signals.py
View file @
85c60670
...
...
@@ -14,7 +14,6 @@ from .choices import ObjectChangeActionChoices
from
.models
import
ConfigRevision
,
CustomField
,
ObjectChange
from
.webhooks
import
enqueue_object
,
get_snapshots
,
serialize_for_webhook
#
# Change logging/webhooks
#
...
...
@@ -100,9 +99,6 @@ def handle_deleted_object(sender, instance, **kwargs):
"""
Fires when an object is deleted.
"""
if
not
hasattr
(
instance
,
'to_objectchange'
):
return
# Get the current request, or bail if not set
request
=
current_request
.
get
()
if
request
is
None
:
...
...
@@ -110,6 +106,8 @@ def handle_deleted_object(sender, instance, **kwargs):
# Record an ObjectChange if applicable
if
hasattr
(
instance
,
'to_objectchange'
):
if
hasattr
(
instance
,
'snapshot'
)
and
not
getattr
(
instance
,
'_prechange_snapshot'
,
None
):
instance
.
snapshot
()
objectchange
=
instance
.
to_objectchange
(
ObjectChangeActionChoices
.
ACTION_DELETE
)
objectchange
.
user
=
request
.
user
objectchange
.
request_id
=
request
.
id
...
...
netbox/extras/tests/test_customfields.py
View file @
85c60670
...
...
@@ -803,6 +803,57 @@ class CustomFieldAPITest(APITestCase):
self
.
assertEqual
(
site2
.
custom_field_data
[
'object_field'
],
original_cfvs
[
'object_field'
])
self
.
assertEqual
(
site2
.
custom_field_data
[
'multiobject_field'
],
original_cfvs
[
'multiobject_field'
])
def
test_specify_related_object_by_attr
(
self
):
site1
=
Site
.
objects
.
get
(
name
=
'Site 1'
)
vlans
=
VLAN
.
objects
.
all
()[:
3
]
url
=
reverse
(
'dcim-api:site-detail'
,
kwargs
=
{
'pk'
:
site1
.
pk
})
self
.
add_permissions
(
'dcim.change_site'
)
# Set related objects by PK
data
=
{
'custom_fields'
:
{
'object_field'
:
vlans
[
0
].
pk
,
'multiobject_field'
:
[
vlans
[
1
].
pk
,
vlans
[
2
].
pk
],
},
}
response
=
self
.
client
.
patch
(
url
,
data
,
format
=
'json'
,
**
self
.
header
)
self
.
assertHttpStatus
(
response
,
status
.
HTTP_200_OK
)
self
.
assertEqual
(
response
.
data
[
'custom_fields'
][
'object_field'
][
'id'
],
vlans
[
0
].
pk
)
self
.
assertListEqual
(
[
obj
[
'id'
]
for
obj
in
response
.
data
[
'custom_fields'
][
'multiobject_field'
]],
[
vlans
[
1
].
pk
,
vlans
[
2
].
pk
]
)
# Set related objects by name
data
=
{
'custom_fields'
:
{
'object_field'
:
{
'name'
:
vlans
[
0
].
name
,
},
'multiobject_field'
:
[
{
'name'
:
vlans
[
1
].
name
},
{
'name'
:
vlans
[
2
].
name
},
],
},
}
response
=
self
.
client
.
patch
(
url
,
data
,
format
=
'json'
,
**
self
.
header
)
self
.
assertHttpStatus
(
response
,
status
.
HTTP_200_OK
)
self
.
assertEqual
(
response
.
data
[
'custom_fields'
][
'object_field'
][
'id'
],
vlans
[
0
].
pk
)
self
.
assertListEqual
(
[
obj
[
'id'
]
for
obj
in
response
.
data
[
'custom_fields'
][
'multiobject_field'
]],
[
vlans
[
1
].
pk
,
vlans
[
2
].
pk
]
)
def
test_minimum_maximum_values_validation
(
self
):
site2
=
Site
.
objects
.
get
(
name
=
'Site 2'
)
url
=
reverse
(
'dcim-api:site-detail'
,
kwargs
=
{
'pk'
:
site2
.
pk
})
...
...
netbox/ipam/forms/models.py
View file @
85c60670
...
...
@@ -429,7 +429,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
initial
[
'nat_rack'
]
=
nat_inside_parent
.
device
.
rack
.
pk
initial
[
'nat_device'
]
=
nat_inside_parent
.
device
.
pk
elif
type
(
nat_inside_parent
)
is
VMInterface
:
initial
[
'nat_cluster'
]
=
nat_inside_parent
.
virtual_machine
.
cluster
.
pk
if
cluster
:
=
nat_inside_parent
.
virtual_machine
.
cluster
:
initial
[
'nat_cluster'
]
=
cluster
.
pk
initial
[
'nat_virtual_machine'
]
=
nat_inside_parent
.
virtual_machine
.
pk
kwargs
[
'initial'
]
=
initial
...
...
netbox/ipam/models/ip.py
View file @
85c60670
...
...
@@ -8,8 +8,6 @@ from django.urls import reverse
from
django.utils.functional
import
cached_property
from
dcim.fields
import
ASNField
from
dcim.models
import
Device
from
netbox.models
import
OrganizationalModel
,
NetBoxModel
from
ipam.choices
import
*
from
ipam.constants
import
*
from
ipam.fields
import
IPNetworkField
,
IPAddressField
...
...
@@ -17,8 +15,7 @@ from ipam.managers import IPAddressManager
from
ipam.querysets
import
PrefixQuerySet
from
ipam.validators
import
DNSValidator
from
netbox.config
import
get_config
from
virtualization.models
import
VirtualMachine
from
netbox.models
import
OrganizationalModel
,
NetBoxModel
__all__
=
(
'Aggregate'
,
...
...
@@ -912,18 +909,6 @@ class IPAddress(NetBoxModel):
)
})
# Check for primary IP assignment that doesn't match the assigned device/VM
if
self
.
pk
:
for
cls
,
attr
in
((
Device
,
'device'
),
(
VirtualMachine
,
'virtual_machine'
)):
parent
=
cls
.
objects
.
filter
(
Q
(
primary_ip4
=
self
)
|
Q
(
primary_ip6
=
self
)).
first
()
if
parent
and
getattr
(
self
.
assigned_object
,
attr
,
None
)
!=
parent
:
# Check for a NAT relationship
if
not
self
.
nat_inside
or
getattr
(
self
.
nat_inside
.
assigned_object
,
attr
,
None
)
!=
parent
:
raise
ValidationError
({
'interface'
:
f
"IP address is primary for
{
cls
.
_meta
.
model_name
}
{
parent
}
but "
f
"not assigned to it!"
})
# Validate IP status selection
if
self
.
status
==
IPAddressStatusChoices
.
STATUS_SLAAC
and
self
.
family
!=
6
:
raise
ValidationError
({
...
...
netbox/netbox/api/viewsets/__init__.py
View file @
85c60670
...
...
@@ -137,9 +137,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Overrides ListModelMixin to allow processing ExportTemplates.
"""
# Overrides ListModelMixin to allow processing ExportTemplates.
if
'export'
in
request
.
GET
:
content_type
=
ContentType
.
objects
.
get_for_model
(
self
.
get_serializer_class
().
Meta
.
model
)
et
=
get_object_or_404
(
ExportTemplate
,
content_type
=
content_type
,
name
=
request
.
GET
[
'export'
])
...
...
netbox/netbox/settings.py
View file @
85c60670
...
...
@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup
#
VERSION
=
'3.3.
8
'
VERSION
=
'3.3.
9
'
# Hostname
HOSTNAME
=
platform
.
node
()
...
...
@@ -445,6 +445,10 @@ EXEMPT_PATHS = (
f
'/
{
BASE_PATH
}
metrics'
,
)
SERIALIZATION_MODULES
=
{
'json'
:
'utilities.serializers.json'
,
}
#
# Sentry
...
...
netbox/netbox/tables/columns.py
View file @
85c60670
...
...
@@ -425,6 +425,12 @@ class CustomFieldColumn(tables.Column):
kwargs
[
'accessor'
]
=
Accessor
(
f
'custom_field_data__
{
customfield
.
name
}
'
)
if
'verbose_name'
not
in
kwargs
:
kwargs
[
'verbose_name'
]
=
customfield
.
label
or
customfield
.
name
# We can't logically sort on FK values
if
customfield
.
type
in
(
CustomFieldTypeChoices
.
TYPE_OBJECT
,
CustomFieldTypeChoices
.
TYPE_MULTIOBJECT
):
kwargs
[
'orderable'
]
=
False
super
().
__init__
(
*
args
,
**
kwargs
)
...
...
netbox/netbox/views/generic/object_views.py
View file @
85c60670
...
...
@@ -125,9 +125,10 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
# Determine the available actions
actions
=
self
.
get_permitted_actions
(
request
.
user
,
model
=
self
.
child_model
)
has_bulk_actions
=
any
([
a
.
startswith
(
'bulk_'
)
for
a
in
actions
])
table_data
=
self
.
prep_table_data
(
request
,
child_objects
,
instance
)
table
=
self
.
get_table
(
table_data
,
request
,
bool
(
actions
)
)
table
=
self
.
get_table
(
table_data
,
request
,
has_bulk_
actions
)
# If this is an HTMX request, return only the rendered table HTML
if
is_htmx
(
request
):
...
...
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment