mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
Implement re-tracing all paths in a migration
This commit is contained in:
parent
c2a63b9960
commit
36ca1b8135
320
netbox/dcim/migrations/0115_retrace.py
Normal file
320
netbox/dcim/migrations/0115_retrace.py
Normal file
@ -0,0 +1,320 @@
|
||||
import sys
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations
|
||||
|
||||
# This migration contains copies of code as it was at the time that this migration was written. This makes sure that
|
||||
# when this migration is run later it will not cause errors if that code has changed in the mean time. We can also not
|
||||
# use isinstance() because migrations create fake classes for models, so they will not be instances of the real class
|
||||
# (if that class even exists at the time the migration is run). So there is a lot of code duplication in here, for good
|
||||
# reasons.
|
||||
|
||||
|
||||
ENDPOINTS = (
|
||||
'ConsolePort',
|
||||
'ConsoleServerPort',
|
||||
'PowerPort',
|
||||
'PowerOutlet',
|
||||
'PowerFeed',
|
||||
'Interface',
|
||||
'CircuitTermination',
|
||||
)
|
||||
|
||||
|
||||
class CableTraceSplit(Exception):
|
||||
"""
|
||||
A cable trace cannot be completed because a RearPort maps to multiple FrontPorts and
|
||||
we don't know which one to follow.
|
||||
"""
|
||||
|
||||
def __init__(self, termination, *_args, **_kwargs):
|
||||
self.termination = termination
|
||||
|
||||
|
||||
def retrace(apps, schema_editor):
|
||||
contenttype_class = apps.get_model('contenttypes', 'ContentType')
|
||||
cable_class = apps.get_model('dcim', 'Cable')
|
||||
|
||||
frontport_class = apps.get_model('dcim', 'FrontPort')
|
||||
rearport_class = apps.get_model('dcim', 'RearPort')
|
||||
|
||||
consoleport_class = apps.get_model('dcim', 'ConsolePort')
|
||||
consoleserverport_class = apps.get_model('dcim', 'ConsoleServerPort')
|
||||
powerport_class = apps.get_model('dcim', 'PowerPort')
|
||||
poweroutlet_class = apps.get_model('dcim', 'PowerOutlet')
|
||||
powerfeed_class = apps.get_model('dcim', 'PowerFeed')
|
||||
interface_class = apps.get_model('dcim', 'Interface')
|
||||
circuittermination_class = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def get_connected_endpoint(cable_termination):
|
||||
"""
|
||||
Because migration models don't have methods we have to reimplement them here.
|
||||
"""
|
||||
if cable_termination is None:
|
||||
return None
|
||||
|
||||
elif cable_termination.__class__.__name__ in ('ConsolePort', 'PowerFeed', 'CircuitTermination'):
|
||||
# These just have their own connected_endpoint field
|
||||
return cable_termination.connected_endpoint
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'ConsoleServerPort':
|
||||
# A ConsoleServerPort connected_endpoint is a reverse OneToOne on a ConsolePort
|
||||
return consoleport_class.objects.filter(connected_endpoint=cable_termination).first()
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'PowerOutlet':
|
||||
# A PowerOutlet connected_endpoint is a reverse OneToOne on a PowerPort
|
||||
return powerport_class.objects.filter(_connected_poweroutlet=cable_termination).first()
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'PowerPort':
|
||||
# Duplicate the PowerPort connected_endpoint getter at the time of writing this migration
|
||||
try:
|
||||
if cable_termination._connected_poweroutlet:
|
||||
return cable_termination._connected_poweroutlet
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
if cable_termination._connected_powerfeed:
|
||||
return cable_termination._connected_powerfeed
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'Interface':
|
||||
# Duplicate the Interface connected_endpoint getter at the time of writing this migration
|
||||
try:
|
||||
if cable_termination._connected_interface:
|
||||
return cable_termination._connected_interface
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
if cable_termination._connected_circuittermination:
|
||||
return cable_termination._connected_circuittermination
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def set_connected_endpoint(cable_termination, new_endpoint):
|
||||
if cable_termination is None:
|
||||
return
|
||||
|
||||
elif cable_termination.__class__.__name__ in ('ConsolePort', 'PowerOutlet', 'CircuitTermination'):
|
||||
# These just have their own connected_endpoint field
|
||||
cable_termination.connected_endpoint = new_endpoint
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'ConsoleServerPort':
|
||||
# A ConsoleServerPort connected_endpoint is a reverse OneToOne on a ConsolePort
|
||||
# Don't do anything here, the other endpoint will set it
|
||||
pass
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'PowerOutlet':
|
||||
# A PowerOutlet connected_endpoint is a reverse OneToOne on a PowerPort
|
||||
# Don't do anything here, the other endpoint will set it
|
||||
pass
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'PowerPort':
|
||||
# Duplicate the PowerPort connected_endpoint setter at the time of writing this migration
|
||||
if new_endpoint.__class__.__name__ == 'PowerOutlet':
|
||||
cable_termination._connected_poweroutlet = new_endpoint
|
||||
cable_termination._connected_powerfeed = None
|
||||
elif new_endpoint.__class__.__name__ == 'PowerFeed':
|
||||
cable_termination._connected_poweroutlet = None
|
||||
cable_termination._connected_powerfeed = new_endpoint
|
||||
else:
|
||||
cable_termination._connected_poweroutlet = None
|
||||
cable_termination._connected_powerfeed = None
|
||||
|
||||
elif cable_termination.__class__.__name__ == 'Interface':
|
||||
# Duplicate the Interface connected_endpoint setter at the time of writing this migration
|
||||
if new_endpoint.__class__.__name__ == 'Interface':
|
||||
cable_termination._connected_interface = new_endpoint
|
||||
cable_termination._connected_circuittermination = None
|
||||
elif new_endpoint.__class__.__name__ == 'CircuitTermination':
|
||||
cable_termination._connected_interface = None
|
||||
cable_termination._connected_circuittermination = new_endpoint
|
||||
else:
|
||||
cable_termination._connected_interface = None
|
||||
cable_termination._connected_circuittermination = None
|
||||
|
||||
def trace(cable_termination):
|
||||
"""
|
||||
Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and
|
||||
the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint
|
||||
along a path which traverses a RearPort. In cases where the originating endpoint is unknown, it is not possible
|
||||
to know which corresponding FrontPort to follow. Remaining positions occur when tracing a path that traverses
|
||||
a FrontPort without traversing a RearPort again.
|
||||
|
||||
The path is a list representing a complete cable path, with each individual segment represented as a
|
||||
three-tuple:
|
||||
|
||||
[
|
||||
(termination A, cable, termination B),
|
||||
(termination C, cable, termination D),
|
||||
(termination E, cable, termination F)
|
||||
]
|
||||
"""
|
||||
trace_endpoint = cable_termination
|
||||
trace_path = []
|
||||
trace_position_stack = []
|
||||
|
||||
def get_peer_port(termination):
|
||||
# Map a front port to its corresponding rear port
|
||||
if termination.__class__.__name__ == 'FrontPort':
|
||||
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
|
||||
peer_port = rearport_class.objects.get(pk=termination.rear_port.pk)
|
||||
|
||||
# Don't use the stack for RearPorts with a single position. Only remember the position at
|
||||
# many-to-one points so we can select the correct FrontPort when we reach the corresponding
|
||||
# one-to-many point.
|
||||
if peer_port.positions > 1:
|
||||
trace_position_stack.append(termination)
|
||||
|
||||
return peer_port
|
||||
|
||||
# Map a rear port/position to its corresponding front port
|
||||
elif termination.__class__.__name__ == 'RearPort':
|
||||
if termination.positions > 1:
|
||||
# Can't map to a FrontPort without a position if there are multiple options
|
||||
if not trace_position_stack:
|
||||
raise CableTraceSplit(termination)
|
||||
|
||||
front_port = trace_position_stack.pop()
|
||||
position = front_port.rear_port_position
|
||||
|
||||
# Validate the position
|
||||
if position not in range(1, termination.positions + 1):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
else:
|
||||
# Don't use the stack for RearPorts with a single position. The only possible position is 1.
|
||||
position = 1
|
||||
|
||||
try:
|
||||
peer_port = frontport_class.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
return peer_port
|
||||
except frontport_class.ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
# Follow a circuit to its other termination
|
||||
elif termination.__class__.__name__ == 'CircuitTermination':
|
||||
peer_side = 'Z' if termination.term_side == 'A' else 'A'
|
||||
try:
|
||||
peer_termination = circuittermination_class.objects.prefetch_related('site').get(
|
||||
circuit=termination.circuit,
|
||||
term_side=peer_side
|
||||
)
|
||||
except circuittermination_class.DoesNotExist:
|
||||
return None
|
||||
|
||||
return peer_termination
|
||||
|
||||
# Termination is not a pass-through port
|
||||
else:
|
||||
return None
|
||||
|
||||
while trace_endpoint is not None:
|
||||
# No cable connected; nothing to trace
|
||||
if not trace_endpoint.cable:
|
||||
trace_path.append((trace_endpoint, None, None))
|
||||
return trace_path, None, trace_position_stack
|
||||
|
||||
# Check for loops
|
||||
if trace_endpoint.cable in [my_segment[1] for my_segment in trace_path]:
|
||||
return trace_path, None, trace_position_stack
|
||||
|
||||
# Record the current segment in the path
|
||||
# noinspection PyProtectedMember
|
||||
endpoint_ct = contenttype_class.objects.get(app_label=trace_endpoint._meta.app_label,
|
||||
model=trace_endpoint._meta.model_name)
|
||||
cable = cable_class.objects.filter(termination_a_type=endpoint_ct,
|
||||
termination_a_id=trace_endpoint.pk).first()
|
||||
if cable:
|
||||
termination_class = apps.get_model(cable.termination_b_type.app_label,
|
||||
cable.termination_b_type.model)
|
||||
far_end = termination_class.objects.get(pk=cable.termination_b_id)
|
||||
else:
|
||||
cable = cable_class.objects.filter(termination_b_type=endpoint_ct,
|
||||
termination_b_id=trace_endpoint.pk).first()
|
||||
if cable:
|
||||
termination_class = apps.get_model(cable.termination_a_type.app_label,
|
||||
cable.termination_a_type.model)
|
||||
far_end = termination_class.objects.get(pk=cable.termination_a_id)
|
||||
else:
|
||||
far_end = None
|
||||
|
||||
trace_path.append((trace_endpoint, trace_endpoint.cable, far_end))
|
||||
|
||||
# Get the peer port of the far end termination
|
||||
try:
|
||||
trace_endpoint = get_peer_port(far_end)
|
||||
except CableTraceSplit as e:
|
||||
return trace_path, e.termination.frontports.all(), trace_position_stack
|
||||
|
||||
if trace_endpoint is None:
|
||||
return trace_path, None, trace_position_stack
|
||||
|
||||
def endpoints():
|
||||
yield from consoleport_class.objects.using(db_alias).all()
|
||||
yield from consoleserverport_class.objects.using(db_alias).all()
|
||||
yield from powerport_class.objects.using(db_alias).all()
|
||||
yield from poweroutlet_class.objects.using(db_alias).all()
|
||||
yield from powerfeed_class.objects.using(db_alias).all()
|
||||
yield from interface_class.objects.using(db_alias).all()
|
||||
yield from circuittermination_class.objects.using(db_alias).all()
|
||||
|
||||
i = 0
|
||||
for endpoint in endpoints():
|
||||
if 'test' not in sys.argv:
|
||||
if i % 100 == 0:
|
||||
print('.', end='', flush=True)
|
||||
i += 1
|
||||
|
||||
path, split_ends, position_stack = trace(endpoint)
|
||||
|
||||
# Determine overall path status (connected or planned)
|
||||
path_status = True
|
||||
for segment in path:
|
||||
if segment[1] is None or segment[1].status != 'connected':
|
||||
path_status = False
|
||||
break
|
||||
|
||||
endpoint_a = path[0][0]
|
||||
if not split_ends and not position_stack:
|
||||
endpoint_b = path[-1][2]
|
||||
if endpoint_b is None and len(path) >= 2 and path[-2][2].__class__.__name__ == 'CircuitTermination':
|
||||
# Simulate the previous behaviour and use the circuit termination as connected endpoint
|
||||
endpoint_b = path[-2][2]
|
||||
else:
|
||||
endpoint_b = None
|
||||
|
||||
# Patch panel ports are not connected endpoints, all other cable terminations are
|
||||
current_endpoint = get_connected_endpoint(endpoint_a)
|
||||
if endpoint_a.__class__.__name__ in ENDPOINTS and \
|
||||
(endpoint_b is None or endpoint_b.__class__.__name__ in ENDPOINTS):
|
||||
if current_endpoint != endpoint_b or endpoint_a.connection_status != path_status:
|
||||
set_connected_endpoint(endpoint_a, endpoint_b)
|
||||
endpoint_a.connection_status = path_status
|
||||
endpoint_a.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0114_update_jsonfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(retrace, migrations.RunPython.noop)
|
||||
]
|
Loading…
Reference in New Issue
Block a user