utils.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. from collections import defaultdict
  2. from django.apps import apps
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.db import router, transaction
  5. from dcim.constants import MODULE_PATH_TOKEN, MODULE_TOKEN, MODULE_TOKEN_SEPARATOR
  6. def resolve_module_placeholders(text, positions):
  7. """
  8. Substitute {module} and {module_path} placeholders in text with position values.
  9. Args:
  10. text: String potentially containing {module} or {module_path} placeholders
  11. positions: List of position strings from the module tree (root to leaf)
  12. Returns:
  13. Text with placeholders replaced according to these rules:
  14. {module_path}: Always expands to full path (positions joined by MODULE_TOKEN_SEPARATOR).
  15. Can only appear once in the text.
  16. {module}: If used once, expands to the PARENT module bay position only (last in positions).
  17. If used multiple times, each token is replaced level-by-level.
  18. This design (Option 2 per sigprof's feedback) allows two approaches:
  19. 1. Use {module_path} for automatic full-path expansion (hardcodes '/' separator)
  20. 2. Use {module} in position fields to build custom paths with user-controlled separators
  21. """
  22. if not text:
  23. return text
  24. result = text
  25. # Handle {module_path} - always expands to full path
  26. if MODULE_PATH_TOKEN in result:
  27. full_path = MODULE_TOKEN_SEPARATOR.join(positions) if positions else ''
  28. result = result.replace(MODULE_PATH_TOKEN, full_path)
  29. # Handle {module} - parent-only for single token, level-by-level for multiple
  30. if MODULE_TOKEN in result:
  31. token_count = result.count(MODULE_TOKEN)
  32. if token_count == 1 and positions:
  33. # Single {module}: substitute with parent (immediate) bay position only
  34. parent_position = positions[-1] if positions else ''
  35. result = result.replace(MODULE_TOKEN, parent_position, 1)
  36. else:
  37. # Multiple {module}: substitute level-by-level (existing behavior)
  38. for pos in positions:
  39. result = result.replace(MODULE_TOKEN, pos, 1)
  40. return result
  41. def compile_path_node(ct_id, object_id):
  42. return f'{ct_id}:{object_id}'
  43. def decompile_path_node(repr):
  44. ct_id, object_id = repr.split(':')
  45. return int(ct_id), int(object_id)
  46. def object_to_path_node(obj):
  47. """
  48. Return a representation of an object suitable for inclusion in a CablePath path. Node representation is in the
  49. form <ContentType ID>:<Object ID>.
  50. """
  51. ct = ContentType.objects.get_for_model(obj)
  52. return compile_path_node(ct.pk, obj.pk)
  53. def path_node_to_object(repr):
  54. """
  55. Given the string representation of a path node, return the corresponding instance. If the object no longer
  56. exists, return None.
  57. """
  58. ct_id, object_id = decompile_path_node(repr)
  59. ct = ContentType.objects.get_for_id(ct_id)
  60. return ct.model_class().objects.filter(pk=object_id).first()
  61. def create_cablepaths(objects):
  62. """
  63. Create CablePaths for all paths originating from the specified set of nodes.
  64. :param objects: Iterable of cabled objects (e.g. Interfaces)
  65. """
  66. from dcim.models import CablePath
  67. # Arrange objects by cable connector. All objects with a null connector are grouped together.
  68. origins = defaultdict(list)
  69. for obj in objects:
  70. origins[obj.cable_connector].append(obj)
  71. for connector, objects in origins.items():
  72. if cp := CablePath.from_origin(objects):
  73. cp.save()
  74. def rebuild_paths(terminations):
  75. """
  76. Rebuild all CablePaths which traverse the specified nodes.
  77. """
  78. from dcim.models import CablePath
  79. for obj in terminations:
  80. cable_paths = CablePath.objects.filter(_nodes__contains=obj)
  81. with transaction.atomic(using=router.db_for_write(CablePath)):
  82. for cp in cable_paths:
  83. cp.delete()
  84. create_cablepaths(cp.origins)
  85. def update_interface_bridges(device, interface_templates, module=None):
  86. """
  87. Used for device and module instantiation. Iterates all InterfaceTemplates with a bridge assigned
  88. and applies it to the actual interfaces.
  89. """
  90. Interface = apps.get_model('dcim', 'Interface')
  91. for interface_template in interface_templates.exclude(bridge=None):
  92. interface = Interface.objects.get(device=device, name=interface_template.resolve_name(module=module))
  93. if interface_template.bridge:
  94. interface.bridge = Interface.objects.get(
  95. device=device,
  96. name=interface_template.bridge.resolve_name(module=module)
  97. )
  98. interface.full_clean()
  99. interface.save()
  100. def create_port_mappings(device, device_type, module=None):
  101. """
  102. Replicate all front/rear port mappings from a DeviceType to the given device.
  103. """
  104. from dcim.models import FrontPort, PortMapping, RearPort
  105. templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port')
  106. # Cache front & rear ports for efficient lookups by name
  107. front_ports = {
  108. fp.name: fp for fp in FrontPort.objects.filter(device=device)
  109. }
  110. rear_ports = {
  111. rp.name: rp for rp in RearPort.objects.filter(device=device)
  112. }
  113. # Replicate PortMappings
  114. mappings = []
  115. for template in templates:
  116. front_port = front_ports.get(template.front_port.resolve_name(module=module))
  117. rear_port = rear_ports.get(template.rear_port.resolve_name(module=module))
  118. mappings.append(
  119. PortMapping(
  120. device_id=front_port.device_id,
  121. front_port=front_port,
  122. front_port_position=template.front_port_position,
  123. rear_port=rear_port,
  124. rear_port_position=template.rear_port_position,
  125. )
  126. )
  127. PortMapping.objects.bulk_create(mappings)