Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v3.6.3 #13907

Merged
merged 20 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9b325f4
PRVB
jeremystretch Sep 20, 2023
b670a1e
Fixes #13871: Fix rack filtering for empty location during device bul…
jeremystretch Sep 25, 2023
df46198
13839 change color and spacing on alert code block (#13857)
arthanson Sep 25, 2023
a0e5e69
#13887: Rebuild static assets
jeremystretch Sep 25, 2023
a8a4bd7
Revert "#13887: Rebuild static assets"
jeremystretch Sep 25, 2023
04796a6
Fix creating config template using rest api (#13869)
desnoe Sep 25, 2023
0ce2b1b
13845 fix device type image save (#13851)
arthanson Sep 25, 2023
685ac5f
13891 fix primary ip assignment if assigning ip
arthanson Sep 25, 2023
27297c7
Add Hide Disconnected Button to Interface Summary, Remove Unused Tabl…
stuntguy3000 Sep 26, 2023
e67624f
Fixes #13666: Fix behavior for reports without test methods (#13667)
JCWasmx86 Sep 26, 2023
f9ceaad
#13666: Add is_valid property to Report class
jeremystretch Sep 26, 2023
099aff5
Changelog for #12732, #13506, #13666, #13839, #13845, #13871, #13891
jeremystretch Sep 26, 2023
3cb41bb
Fixes #13849: Fix label resolution during serialization for removed f…
jeremystretch Sep 26, 2023
b759d69
Fixes #13859: Fix valid response when no matching choice values are f…
jeremystretch Sep 25, 2023
1ad6d94
Fixes #13843: Fix assignment of VLAN group scope during bulk edit (#1…
jeremystretch Sep 26, 2023
f65744f
Fixes: #11079 - Handle cables across multiple rear-port positions (#1…
DanSheps Sep 26, 2023
db40119
13130 dont allow reassigning ipaddress assigned object if primary ip …
arthanson Sep 26, 2023
4dd229e
Fixes #13864: Remove 'default' choice for dashboard widget color
jeremystretch Sep 22, 2023
1a00765
Changelog for #11079, #11901, #13843, #13849, #13859, #13864
jeremystretch Sep 26, 2023
9e35cef
Release v3.6.3
jeremystretch Sep 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.6.2
placeholder: v3.6.3
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.6.2
placeholder: v3.6.3
validations:
required: true
- type: dropdown
Expand Down
23 changes: 23 additions & 0 deletions docs/release-notes/version-3.6.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# NetBox v3.6

## v3.6.3 (2023-09-26)

### Enhancements

* [#12732](https://github.com/netbox-community/netbox/issues/12732) - Add toggle to hide disconnected interfaces under device view

### Bug Fixes

* [#11079](https://github.com/netbox-community/netbox/issues/11079) - Enable tracing cable paths across multiple cables in parallel
* [#11901](https://github.com/netbox-community/netbox/issues/11901) - Fix `IndexError` exception when manipulating terminations for existing cables via REST API
* [#13506](https://github.com/netbox-community/netbox/issues/13506) - Enable creating a config template which references a data file via the REST API
* [#13666](https://github.com/netbox-community/netbox/issues/13666) - Cleanly handle reports without any test methods defined
* [#13839](https://github.com/netbox-community/netbox/issues/13839) - Restore original text color for HTML code elements
* [#13843](https://github.com/netbox-community/netbox/issues/13843) - Fix assignment of VLAN group scope during bulk edit
* [#13845](https://github.com/netbox-community/netbox/issues/13845) - Fix `AttributeError` exception when attaching front/rear images to a device type
* [#13849](https://github.com/netbox-community/netbox/issues/13849) - Fix `KeyError` exception when deleting an object which references a configured choice value that has been removed
* [#13859](https://github.com/netbox-community/netbox/issues/13859) - Fix invalid response when searching for custom choice field values returns no matches
* [#13864](https://github.com/netbox-community/netbox/issues/13864) - Correct default background color for dashboard widget headers
* [#13871](https://github.com/netbox-community/netbox/issues/13871) - Fix rack filtering for empty location during device bulk import
* [#13891](https://github.com/netbox-community/netbox/issues/13891) - Allow designating an IP address as primary for device/VM while assigning it to an interface

---

## v3.6.2 (2023-09-20)

### Enhancements
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,9 @@ def __init__(self, data=None, *args, **kwargs):
params = {
f"site__{self.fields['site'].to_field_name}": data.get('site'),
}
if 'location' in data:
if location := data.get('location'):
params.update({
f"location__{self.fields['location'].to_field_name}": data.get('location'),
f"location__{self.fields['location'].to_field_name}": location,
})
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)

Expand Down
122 changes: 91 additions & 31 deletions netbox/dcim/models/cables.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
from wireless.models import WirelessLink
from .device_components import FrontPort, RearPort
from .device_components import FrontPort, RearPort, PathEndpoint

__all__ = (
'Cable',
Expand Down Expand Up @@ -518,9 +518,16 @@ def from_origin(cls, terminations):
# Terminations must all be of the same type
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])

# All mid-span terminations must all be attached to the same device
if not isinstance(terminations[0], PathEndpoint):
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
assert all(t.parent_object == terminations[0].parent_object for t in terminations[1:])

# Check for a split path (e.g. rear port fanning out to multiple front ports with
# different cables attached)
if len(set(t.link for t in terminations)) > 1:
if len(set(t.link for t in terminations)) > 1 and (
position_stack and len(terminations) != len(position_stack[-1])
):
is_split = True
break

Expand All @@ -529,46 +536,68 @@ def from_origin(cls, terminations):
object_to_path_node(t) for t in terminations
])

# Step 2: Determine the attached link (Cable or WirelessLink), if any
link = terminations[0].link
if link is None and len(path) == 1:
# If this is the start of the path and no link exists, return None
return None
elif link is None:
# Step 2: Determine the attached links (Cable or WirelessLink), if any
links = [termination.link for termination in terminations if termination.link is not None]
if len(links) == 0:
if len(path) == 1:
# If this is the start of the path and no link exists, return None
return None
# Otherwise, halt the trace if no link exists
break
assert type(link) in (Cable, WirelessLink)
assert all(type(link) in (Cable, WirelessLink) for link in links)
assert all(isinstance(link, type(links[0])) for link in links)

# Step 3: Record asymmetric paths as split
not_connected_terminations = [termination.link for termination in terminations if termination.link is None]
if len(not_connected_terminations) > 0:
is_complete = False
is_split = True

# Step 3: Record the link and update path status if not "connected"
path.append([object_to_path_node(link)])
if hasattr(link, 'status') and link.status != LinkStatusChoices.STATUS_CONNECTED:
# Step 4: Record the links, keeping cables in order to allow for SVG rendering
cables = []
for link in links:
if object_to_path_node(link) not in cables:
cables.append(object_to_path_node(link))
path.append(cables)

# Step 5: Update the path status if a link is not connected
links_status = [link.status for link in links if link.status != LinkStatusChoices.STATUS_CONNECTED]
if any([status != LinkStatusChoices.STATUS_CONNECTED for status in links_status]):
is_active = False

# Step 4: Determine the far-end terminations
if isinstance(link, Cable):
# Step 6: Determine the far-end terminations
if isinstance(links[0], Cable):
termination_type = ContentType.objects.get_for_model(terminations[0])
local_cable_terminations = CableTermination.objects.filter(
termination_type=termination_type,
termination_id__in=[t.pk for t in terminations]
)
# Terminations must all belong to same end of Cable
local_cable_end = local_cable_terminations[0].cable_end
assert all(ct.cable_end == local_cable_end for ct in local_cable_terminations[1:])
remote_cable_terminations = CableTermination.objects.filter(
cable=link,
cable_end='A' if local_cable_end == 'B' else 'B'
)

q_filter = Q()
for lct in local_cable_terminations:
cable_end = 'A' if lct.cable_end == 'B' else 'B'
q_filter |= Q(cable=lct.cable, cable_end=cable_end)

remote_cable_terminations = CableTermination.objects.filter(q_filter)
remote_terminations = [ct.termination for ct in remote_cable_terminations]
else:
# WirelessLink
remote_terminations = [link.interface_b] if link.interface_a is terminations[0] else [link.interface_a]
remote_terminations = [
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
]

# Remote Terminations must all be of the same type, otherwise return a split path
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
is_complete = False
is_split = True
break

# Step 5: Record the far-end termination object(s)
# Step 7: Record the far-end termination object(s)
path.append([
object_to_path_node(t) for t in remote_terminations if t is not None
])

# Step 6: Determine the "next hop" terminations, if applicable
# Step 8: Determine the "next hop" terminations, if applicable
if not remote_terminations:
break

Expand All @@ -577,20 +606,32 @@ def from_origin(cls, terminations):
rear_ports = RearPort.objects.filter(
pk__in=[t.rear_port_id for t in remote_terminations]
)
if len(rear_ports) > 1:
assert all(rp.positions == 1 for rp in rear_ports)
elif rear_ports[0].positions > 1:
if len(rear_ports) > 1 or rear_ports[0].positions > 1:
position_stack.append([fp.rear_port_position for fp in remote_terminations])

terminations = rear_ports

elif isinstance(remote_terminations[0], RearPort):

if len(remote_terminations) > 1 or remote_terminations[0].positions == 1:
if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
front_ports = FrontPort.objects.filter(
rear_port_id__in=[rp.pk for rp in remote_terminations],
rear_port_position=1
)
# Obtain the individual front ports based on the termination and all positions
elif len(remote_terminations) > 1 and position_stack:
positions = position_stack.pop()

# Ensure we have a number of positions equal to the amount of remote terminations
assert len(remote_terminations) == len(positions)

# Get our front ports
q_filter = Q()
for rt in remote_terminations:
position = positions.pop()
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
assert q_filter is not Q()
front_ports = FrontPort.objects.filter(q_filter)
# Obtain the individual front ports based on the termination and position
elif position_stack:
front_ports = FrontPort.objects.filter(
rear_port_id=remote_terminations[0].pk,
Expand Down Expand Up @@ -632,9 +673,16 @@ def from_origin(cls, terminations):

terminations = [circuit_termination]

# Anything else marks the end of the path
else:
is_complete = True
# Check for non-symmetric path
if all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
is_complete = True
elif len(remote_terminations) == 0:
is_complete = False
else:
# Unsupported topology, mark as split and exit
is_complete = False
is_split = True
break

return cls(
Expand Down Expand Up @@ -740,3 +788,15 @@ def get_split_nodes(self):
return [
ct.get_peer_termination() for ct in nodes
]

def get_asymmetric_nodes(self):
"""
Return all available next segments in a split cable path.
"""
from circuits.models import CircuitTermination
asymmetric_nodes = []
for nodes in self.path_objects:
if type(nodes[0]) in [RearPort, FrontPort, CircuitTermination]:
asymmetric_nodes.extend([node for node in nodes if node.link is None])

return asymmetric_nodes
9 changes: 5 additions & 4 deletions netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from functools import cached_property

from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import F, ProtectedError
Expand Down Expand Up @@ -332,10 +333,10 @@ def save(self, *args, **kwargs):
ret = super().save(*args, **kwargs)

# Delete any previously uploaded image files that are no longer in use
if self.front_image != self._original_front_image:
self._original_front_image.delete(save=False)
if self.rear_image != self._original_rear_image:
self._original_rear_image.delete(save=False)
if self._original_front_image and self.front_image != self._original_front_image:
default_storage.delete(self._original_front_image)
if self._original_rear_image and self.rear_image != self._original_rear_image:
default_storage.delete(self._original_rear_image)

return ret

Expand Down
Loading
Loading