netbox-data-import 2.3.0__tar.gz → 2.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/PKG-INFO +3 -3
  2. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/README.md +2 -2
  3. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/__init__.py +1 -1
  4. netbox_data_import-2.5.0/netbox_data_import/_dns_worker.py +31 -0
  5. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/adapter_forms.py +1 -1
  6. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/adapters.py +53 -2
  7. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/cable_target.py +48 -45
  8. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/catalog.py +1 -0
  9. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/device_field_review.py +45 -3
  10. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/forms.py +24 -3
  11. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/import_engine.py +3 -1
  12. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/inference_adapter.py +143 -48
  13. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/inference_backend.py +18 -0
  14. netbox_data_import-2.5.0/netbox_data_import/inference_connection_test.py +146 -0
  15. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/inference_credentials.py +68 -24
  16. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/inference_settings.py +3 -4
  17. netbox_data_import-2.5.0/netbox_data_import/inference_transport.py +323 -0
  18. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/inference_trust.py +109 -2
  19. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/ip_assignment.py +72 -0
  20. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/jobs.py +0 -23
  21. netbox_data_import-2.5.0/netbox_data_import/migrations/0034_tracedeviceresolution.py +41 -0
  22. netbox_data_import-2.5.0/netbox_data_import/migrations/0035_retire_superseded_proposals.py +28 -0
  23. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/models.py +69 -11
  24. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/netbox_reader.py +6 -0
  25. netbox_data_import-2.5.0/netbox_data_import/object_permissions.py +677 -0
  26. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/profile_yaml.py +168 -60
  27. netbox_data_import-2.5.0/netbox_data_import/proposal_contract.py +30 -0
  28. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/proposal_jobs.py +12 -17
  29. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/proposal_response.py +36 -16
  30. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/review_workspace.py +74 -12
  31. netbox_data_import-2.5.0/netbox_data_import/source_trace.py +124 -0
  32. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +3 -1
  33. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +15 -11
  34. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +2 -0
  35. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +126 -4
  36. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +149 -54
  37. netbox_data_import-2.5.0/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +130 -0
  38. netbox_data_import-2.5.0/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +54 -0
  39. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +16 -37
  40. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/target_modules.py +265 -84
  41. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/_proposal_card.html +9 -9
  42. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +71 -6
  43. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +1 -1
  44. netbox_data_import-2.5.0/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +45 -0
  45. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +20 -0
  46. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +218 -39
  47. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/termination_proposal.py +14 -6
  48. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/helpers.py +40 -5
  49. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/inference_http.py +7 -2
  50. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/mock_discipline.py +2 -1
  51. netbox_data_import-2.5.0/netbox_data_import/tests/redaction.py +54 -0
  52. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_adapter_config_migration.py +2 -2
  53. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_atomic_exits.py +7 -3
  54. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_cable_module.py +75 -6
  55. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_ci_configuration.py +14 -28
  56. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_comment_style.py +2 -2
  57. netbox_data_import-2.5.0/netbox_data_import/tests/test_configuration_docs.py +147 -0
  58. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_configuration_surfaces.py +317 -0
  59. netbox_data_import-2.5.0/netbox_data_import/tests/test_conflict_row_actions.py +241 -0
  60. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_contact_sync.py +4 -45
  61. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +10 -60
  62. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_device_module.py +190 -15
  63. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_digest_indexed_models.py +18 -1
  64. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_execution_audit.py +1 -1
  65. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_import_cutover.py +194 -23
  66. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_import_engine.py +109 -2
  67. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_import_execution.py +105 -8
  68. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_import_source_migration.py +2 -2
  69. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_inference_adapter.py +323 -4
  70. netbox_data_import-2.5.0/netbox_data_import/tests/test_inference_connection_test.py +743 -0
  71. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_inference_credentials.py +80 -50
  72. netbox_data_import-2.5.0/netbox_data_import/tests/test_inference_secret_containment.py +338 -0
  73. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_inference_settings.py +6 -3
  74. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_inference_trust.py +228 -0
  75. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_installation_docs.py +1 -1
  76. netbox_data_import-2.5.0/netbox_data_import/tests/test_markdown_tables.py +124 -0
  77. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_migrations.py +2 -1
  78. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_mock_discipline.py +21 -5
  79. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_module_boundaries.py +76 -12
  80. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_object_permission_writer.py +609 -3
  81. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_placement_reference_locks.py +1 -1
  82. netbox_data_import-2.5.0/netbox_data_import/tests/test_policy_write_refusals.py +155 -0
  83. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_preview_assets.py +483 -10
  84. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_pristine_preview_rows.py +3 -19
  85. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_proposal_jobs.py +82 -4
  86. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_proposal_response.py +36 -16
  87. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_proposal_tasks.py +12 -2
  88. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_proposal_workspace.py +20 -3
  89. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +21 -3
  90. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_rack_module.py +53 -1
  91. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_release_metadata.py +3 -3
  92. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_resolution_proposal_api.py +4 -3
  93. netbox_data_import-2.5.0/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +173 -0
  94. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_review_actions.py +14 -0
  95. netbox_data_import-2.5.0/netbox_data_import/tests/test_source_trace_contract.py +66 -0
  96. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_standard_views.py +23 -1
  97. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_target_field_catalog.py +1 -1
  98. netbox_data_import-2.5.0/netbox_data_import/tests/test_target_module_edge_contracts.py +1069 -0
  99. netbox_data_import-2.5.0/netbox_data_import/tests/test_text_encoding.py +116 -0
  100. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_trace_decisions.py +1 -1
  101. netbox_data_import-2.5.0/netbox_data_import/tests/test_trace_device_resolution.py +995 -0
  102. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_trace_workbook.py +0 -29
  103. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_trace_workspace.py +223 -15
  104. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_views.py +63 -5
  105. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_views_coverage2.py +11 -8
  106. netbox_data_import-2.5.0/netbox_data_import/trace_device_resolution.py +482 -0
  107. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/trace_workbook.py +19 -104
  108. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/urls.py +11 -0
  109. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/views.py +541 -99
  110. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import.egg-info/PKG-INFO +3 -3
  111. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import.egg-info/SOURCES.txt +19 -2
  112. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/pyproject.toml +18 -6
  113. netbox_data_import-2.3.0/netbox_data_import/inference_connection_test.py +0 -73
  114. netbox_data_import-2.3.0/netbox_data_import/inference_transport.py +0 -130
  115. netbox_data_import-2.3.0/netbox_data_import/object_permissions.py +0 -340
  116. netbox_data_import-2.3.0/netbox_data_import/tests/test_inference_connection_test.py +0 -452
  117. netbox_data_import-2.3.0/netbox_data_import/tests/test_inference_secret_containment.py +0 -216
  118. netbox_data_import-2.3.0/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -171
  119. netbox_data_import-2.3.0/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -449
  120. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/LICENSE +0 -0
  121. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/adapter_config.py +0 -0
  122. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/api/__init__.py +0 -0
  123. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/api/serializers.py +0 -0
  124. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/api/urls.py +0 -0
  125. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/api/views.py +0 -0
  126. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/contact_resolution.py +0 -0
  127. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/device_identity.py +0 -0
  128. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/field_keys.py +0 -0
  129. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/filters.py +0 -0
  130. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/flat_workbook.py +0 -0
  131. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/graphql/__init__.py +0 -0
  132. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/graphql/filters.py +0 -0
  133. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/graphql/schema.py +0 -0
  134. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/graphql/types.py +0 -0
  135. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0001_initial.py +0 -0
  136. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  137. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
  138. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0004_features.py +0 -0
  139. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  140. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  141. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  142. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  143. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  144. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  145. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  146. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  147. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  148. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  149. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  150. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  151. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  152. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  153. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  154. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  155. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  156. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
  157. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  158. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  159. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  160. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  161. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  162. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  163. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  164. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  165. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
  166. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
  167. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
  168. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/migrations/__init__.py +0 -0
  169. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/navigation.py +0 -0
  170. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/plan.py +0 -0
  171. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/preview_row_actions.py +0 -0
  172. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/proposal_decisions.py +0 -0
  173. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/proposal_presentation.py +0 -0
  174. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/proposal_tasks.py +0 -0
  175. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/resolution_proposals.py +0 -0
  176. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/signals.py +0 -0
  177. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/source_resolution.py +0 -0
  178. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  179. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +0 -0
  180. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tables.py +0 -0
  181. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/target_runtime.py +0 -0
  182. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/template_content.py +0 -0
  183. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  184. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
  185. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  186. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  187. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  188. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  189. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  190. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  191. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  192. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  193. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  194. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  195. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  196. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  197. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  198. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  199. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  200. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/__init__.py +0 -0
  201. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/conftest.py +0 -0
  202. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  203. /netbox_data_import-2.3.0/netbox_data_import/tests/fixtures/sample_cans.xlsx → /netbox_data_import-2.5.0/netbox_data_import/tests/fixtures/sample_workbook.xlsx +0 -0
  204. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  205. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  206. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/isolated_settings.py +0 -0
  207. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/mixins.py +0 -0
  208. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  209. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/parallel.py +0 -0
  210. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  211. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_api.py +0 -0
  212. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +0 -0
  213. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  214. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
  215. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  216. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_import_plan.py +0 -0
  217. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
  218. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  219. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
  220. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  221. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_models.py +0 -0
  222. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_navigation.py +0 -0
  223. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  224. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  225. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
  226. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  227. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_proposal_decisions.py +0 -0
  228. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  229. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  230. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
  231. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_resolution_proposals.py +0 -0
  232. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_review_resolutions.py +0 -0
  233. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
  234. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  235. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  236. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  237. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/trace_schema.py +0 -0
  238. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/transform_regex.py +0 -0
  239. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/utils.py +0 -0
  240. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import/values.py +0 -0
  241. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  242. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import.egg-info/requires.txt +0 -0
  243. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  244. {netbox_data_import-2.3.0 → netbox_data_import-2.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-data-import
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: NetBox plugin for importing data from external DCIM systems
5
5
  Author: Marcin Zieba
6
6
  License-Expression: Apache-2.0
@@ -39,14 +39,14 @@ A NetBox plugin for importing device inventory and rack layouts from external DC
39
39
 
40
40
  This plugin allows you to:
41
41
 
42
- - Define **import profiles** that configure how your source data (Excel/CSV) maps to NetBox objects
42
+ - Define **import profiles** that configure how your source data (Excel) maps to NetBox objects
43
43
  - Configure **field mappings** per column with transformation rules
44
44
  - Define **class/role mappings** to translate your source system's device categories to NetBox roles
45
45
  - Synchronize a mapped **primary contact** into native NetBox Contacts and Contact Assignments
46
46
  - **Preview** the import before applying changes
47
47
  - **Run imports in the background**, track processed rows, and resume the progress page
48
48
 
49
- Currently supports importing from Excel (`.xlsx`) files in the CANS format, with the mapping configuration stored in NetBox so it can be reused and adapted for other source formats.
49
+ Currently supports importing from Excel (`.xlsx`) workbooks with a flat, one-row-per-device layout. The mapping configuration is stored in NetBox, so it can be reused and adapted for other source formats.
50
50
 
51
51
  ## Requirements
52
52
 
@@ -16,14 +16,14 @@ A NetBox plugin for importing device inventory and rack layouts from external DC
16
16
 
17
17
  This plugin allows you to:
18
18
 
19
- - Define **import profiles** that configure how your source data (Excel/CSV) maps to NetBox objects
19
+ - Define **import profiles** that configure how your source data (Excel) maps to NetBox objects
20
20
  - Configure **field mappings** per column with transformation rules
21
21
  - Define **class/role mappings** to translate your source system's device categories to NetBox roles
22
22
  - Synchronize a mapped **primary contact** into native NetBox Contacts and Contact Assignments
23
23
  - **Preview** the import before applying changes
24
24
  - **Run imports in the background**, track processed rows, and resume the progress page
25
25
 
26
- Currently supports importing from Excel (`.xlsx`) files in the CANS format, with the mapping configuration stored in NetBox so it can be reused and adapted for other source formats.
26
+ Currently supports importing from Excel (`.xlsx`) workbooks with a flat, one-row-per-device layout. The mapping configuration is stored in NetBox, so it can be reused and adapted for other source formats.
27
27
 
28
28
  ## Requirements
29
29
 
@@ -4,7 +4,7 @@ from typing import Any
4
4
 
5
5
  from netbox.plugins import PluginConfig
6
6
 
7
- __version__ = "2.3.0"
7
+ __version__ = "2.5.0"
8
8
 
9
9
 
10
10
  class NetBoxDataImportConfig(PluginConfig):
@@ -0,0 +1,31 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
3
+ """Resolve one host with libc in a deadline-bounded child process."""
4
+
5
+ import json
6
+ import signal
7
+ import socket
8
+ import sys
9
+
10
+ from typing import TextIO
11
+
12
+
13
+ def main(stdin: TextIO, stdout: TextIO) -> int:
14
+ """Read one request from stdin and write its ordered addresses to stdout."""
15
+ try:
16
+ host, port, timeout = json.load(stdin)
17
+ except Exception: # noqa: BLE001 - the parent exposes one typed resolution failure
18
+ return 1
19
+ # The alarm is the deadline. An unarmed worker must crash, not read as a failed lookup.
20
+ signal.setitimer(signal.ITIMER_REAL, timeout)
21
+ try:
22
+ answers = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
23
+ addresses = list(dict.fromkeys(str(answer[4][0]) for answer in answers))
24
+ except Exception: # noqa: BLE001 - the parent exposes one typed resolution failure
25
+ return 1
26
+ json.dump(addresses, stdout)
27
+ return 0
28
+
29
+
30
+ if __name__ == "__main__": # pragma: no cover - child entry point; main() is tested in process
31
+ raise SystemExit(main(sys.stdin, sys.stdout))
@@ -91,7 +91,7 @@ class FlatWorkbookConfigForm(AdapterConfigForm):
91
91
  max_length=100,
92
92
  required=False,
93
93
  initial="",
94
- help_text="NetBox custom field name to store the source ID in (e.g. 'cans_id')",
94
+ help_text="NetBox custom field name to store the source ID in (e.g. 'external_id')",
95
95
  )
96
96
  update_existing = forms.BooleanField(
97
97
  required=False,
@@ -13,13 +13,14 @@ the NetBox boundary.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from dataclasses import dataclass, field
16
+ from dataclasses import dataclass, field, replace
17
17
  from typing import TYPE_CHECKING
18
18
 
19
19
  from .catalog import OutputKind, has_implemented_module
20
+ from .values import identity_text
20
21
 
21
22
  if TYPE_CHECKING:
22
- from .trace_workbook import SourceTrace
23
+ from .source_trace import SourceTrace
23
24
 
24
25
 
25
26
  class UnknownSourceAdapter(Exception):
@@ -39,6 +40,16 @@ class SourceDiagnostic:
39
40
  row_number: int | None = None
40
41
 
41
42
 
43
+ def _foreign_row_number(row) -> int | None:
44
+ """Return the source row a foreign row states, so its diagnostic can name a location."""
45
+ provenance = getattr(row, "provenance", ())
46
+ if provenance:
47
+ return provenance[0].row_start
48
+ if isinstance(row, dict):
49
+ return row.get("_row_number")
50
+ return None
51
+
52
+
42
53
  @dataclass(frozen=True)
43
54
  class SourceBatch:
44
55
  """The typed source items and source diagnostics from one file (section 1)."""
@@ -48,6 +59,46 @@ class SourceBatch:
48
59
  diagnostics: tuple[SourceDiagnostic, ...] = ()
49
60
  unused_columns: dict[str, dict] = field(default_factory=dict)
50
61
 
62
+ def __post_init__(self):
63
+ """Exclude a row the output kinds cannot carry, and mark a Trace with empty Device references."""
64
+ from .source_trace import SourceTrace
65
+
66
+ trace_batch = OutputKind.SOURCE_TRACE in self.output_kinds
67
+ expected = SourceTrace if trace_batch else dict
68
+ carries = "Source Traces" if trace_batch else "source rows"
69
+ rows = []
70
+ diagnostics = list(self.diagnostics)
71
+ for row in self.rows:
72
+ if not isinstance(row, expected):
73
+ # Excluded here, because every reader below reads the row the output kinds declare.
74
+ diagnostics.append(
75
+ SourceDiagnostic(
76
+ code="source.row_type_unexpected",
77
+ message=f"This batch carries {carries} only; got {type(row).__name__}.",
78
+ row_number=_foreign_row_number(row),
79
+ )
80
+ )
81
+ continue
82
+ if not trace_batch:
83
+ rows.append(row)
84
+ continue
85
+ summary = row.endpoint_summary
86
+ references = [summary.from_termination, summary.to_termination, *row.corroboration]
87
+ for segment in row.segments:
88
+ references.extend((segment.left, segment.right))
89
+ if all(identity_text(reference.device) for reference in references):
90
+ rows.append(row)
91
+ continue
92
+ diagnostic = SourceDiagnostic(
93
+ code="trace.device_required",
94
+ message="A Source Trace Termination Reference must name a Device.",
95
+ row_number=row.provenance[0].row_start,
96
+ )
97
+ diagnostics.append(diagnostic)
98
+ rows.append(replace(row, errors=(*row.errors, diagnostic)))
99
+ object.__setattr__(self, "rows", tuple(rows))
100
+ object.__setattr__(self, "diagnostics", tuple(diagnostics))
101
+
51
102
 
52
103
  class SourceAdapter:
53
104
  """Base class for a Source Adapter declaration."""
@@ -36,6 +36,13 @@ from .field_keys import (
36
36
  from .object_permissions import enforce_saved_object_permission
37
37
  from .plan import Diagnostic, Disposition, PlannedChange, Severity, SynchronizationUnit
38
38
  from .target_runtime import DeletedObject, PreconditionFailed
39
+ from .trace_device_resolution import (
40
+ STALE as DEVICE_STALE,
41
+ collect_trace_device_evidence,
42
+ resolve_trace_devices,
43
+ resolved_trace_device,
44
+ source_device_key,
45
+ )
39
46
  from .values import identity_text, source_text
40
47
 
41
48
  CABLE_STATUS = "connected"
@@ -111,6 +118,7 @@ class _Termination:
111
118
  kind: str
112
119
  device_id: int
113
120
  display: str
121
+ topology_display: str
114
122
 
115
123
  @property
116
124
  def key(self) -> tuple[str, int]:
@@ -207,7 +215,9 @@ class _TraceAnalysis:
207
215
  segments: list = field(default_factory=list)
208
216
  proven: dict = field(default_factory=dict)
209
217
  policies: dict = field(default_factory=dict)
218
+ devices: dict = field(default_factory=dict)
210
219
  terminations: dict = field(default_factory=dict)
220
+ resolution_started: bool = False
211
221
  topology_read: bool = False
212
222
  logical_cable: Any = None
213
223
  retains_logical_cable: bool = False
@@ -303,38 +313,19 @@ def _source_record(trace, segment_index: int) -> dict:
303
313
  }
304
314
 
305
315
 
306
- def _visible_devices(netbox_reader, names, *, _lock_rows=False) -> dict[str, list]:
307
- """Return the visible Devices at the import target, grouped by comparison name."""
308
- from django.db.models import Q
309
-
310
- wanted = {identity_text(name) for name in names if identity_text(name)}
311
- if not wanted:
312
- return {}
313
- devices = netbox_reader.devices()
314
- if netbox_reader.site is not None:
315
- devices = devices.filter(site=netbox_reader.site)
316
- if _lock_rows:
317
- devices = devices.order_by("pk").select_for_update(of=("self",))
318
- lookup = Q()
319
- for name in sorted(wanted):
320
- lookup |= Q(name__iexact=name)
321
- grouped: dict[str, list] = {}
322
- for device in devices.filter(lookup):
323
- comparison = identity_text(device.name)
324
- if comparison in wanted:
325
- grouped.setdefault(comparison, []).append(device)
326
- return grouped
327
-
328
-
329
- def resolved_device_for(field_key: str, netbox_reader, *, _lock_rows=False):
316
+ def resolved_device_for(field_key: str, netbox_reader, *, profile, _lock_rows=False):
330
317
  """Return the one visible Device a termination field key names, or None when it names no single one.
331
318
 
332
319
  A proposal freezes this reference and revalidates it, so the retrieval and the freshness check
333
320
  have to agree on what "the resolved Device" means.
334
321
  """
335
322
  device_name = parse_termination_field_key(field_key)["device"]
336
- devices = _visible_devices(netbox_reader, [device_name], _lock_rows=_lock_rows).get(identity_text(device_name), [])
337
- return devices[0] if len(devices) == 1 else None
323
+ return resolved_trace_device(
324
+ profile=profile,
325
+ reader=netbox_reader,
326
+ source_label=device_name,
327
+ lock_rows=_lock_rows,
328
+ )
338
329
 
339
330
 
340
331
  _RESOLVED_DEVICE_UNSET = object()
@@ -361,7 +352,7 @@ def eligible_terminations(
361
352
  kind = parsed["kind"]
362
353
  accessor = _READER_ACCESSOR_BY_KIND[kind]
363
354
  device = (
364
- resolved_device_for(field_key, netbox_reader, _lock_rows=_lock_rows)
355
+ resolved_device_for(field_key, netbox_reader, profile=profile, _lock_rows=_lock_rows)
365
356
  if _resolved_device is _RESOLVED_DEVICE_UNSET
366
357
  else _resolved_device
367
358
  )
@@ -430,6 +421,7 @@ class _CableBatch:
430
421
  """Plan every Source Trace in one batch, so an identical shared segment plans once."""
431
422
 
432
423
  def __init__(self, traces, profile, netbox_reader, *, lock_plan_references: bool = False):
424
+ traces = tuple(traces)
433
425
  self.profile = profile
434
426
  self.reader = netbox_reader
435
427
  self.actor = netbox_reader.actor
@@ -445,7 +437,19 @@ class _CableBatch:
445
437
  self._occupied: dict[tuple[str, int], _ExistingCable] = {}
446
438
  self._mapping_rows: dict[str, Any] | None = None
447
439
  self._stored = self._stored_resolutions()
448
- self._devices = self._load_devices()
440
+ active_traces = (analysis.trace for analysis in self.analyses if not analysis.stopped)
441
+ self._device_evidence = collect_trace_device_evidence(active_traces)
442
+ self._device_resolutions = resolve_trace_devices(
443
+ profile=self.profile,
444
+ reader=self.reader,
445
+ evidence=self._device_evidence,
446
+ lock_rows=self.lock_plan_references,
447
+ )
448
+ self._device_displays = {
449
+ resolution.device.pk: str(resolution.device)
450
+ for resolution in self._device_resolutions.values()
451
+ if resolution.device is not None
452
+ }
449
453
  self._resolve_terminations()
450
454
  self._load_mappings()
451
455
  self._build_segments()
@@ -512,16 +516,6 @@ class _CableBatch:
512
516
  ).select_related("selected_object_type")
513
517
  return {row.field_key: row for row in rows}
514
518
 
515
- def _load_devices(self) -> dict[str, list]:
516
- """Return the visible Devices every planned trace names, grouped by comparison name."""
517
- names = [
518
- reference.device
519
- for analysis in self.analyses
520
- if not analysis.stopped
521
- for reference in self._references(analysis.trace)
522
- ]
523
- return _visible_devices(self.reader, names)
524
-
525
519
  def _components_for(self, device_id: int, kind: str) -> dict[str, list]:
526
520
  """Return one Device's terminations of one kind, grouped by comparison name."""
527
521
  cached = self._components.get((device_id, kind))
@@ -537,6 +531,7 @@ class _CableBatch:
537
531
  for analysis in self.analyses:
538
532
  if analysis.stopped:
539
533
  continue
534
+ analysis.resolution_started = True
540
535
  resolved: dict[tuple, _Termination] = {}
541
536
  for reference in self._references(analysis.trace):
542
537
  if reference.identity_key in resolved:
@@ -548,19 +543,24 @@ class _CableBatch:
548
543
 
549
544
  def _resolve_one(self, analysis: _TraceAnalysis, reference) -> _Termination | None:
550
545
  """Return the NetBox object one Termination Reference names, or record the open decision."""
551
- devices = self._devices.get(identity_text(reference.device), [])
552
- if len(devices) != 1:
553
- analysis.block("trace.device_unresolved", {**_reference_display(reference), "matches": len(devices)})
546
+ resolution = self._device_resolutions[source_device_key(reference.device)]
547
+ analysis.devices.setdefault(resolution.evidence.key, resolution.to_question())
548
+ if resolution.device is None:
549
+ code = "trace.device_resolution_stale" if resolution.state == DEVICE_STALE else "trace.device_unresolved"
550
+ analysis.block(
551
+ code,
552
+ {**_reference_display(reference), "matches": resolution.exact_match_count},
553
+ )
554
554
  # With no resolved Device there is nothing to pick from, so the picker cannot help here.
555
555
  self._record_resolution(
556
556
  analysis,
557
557
  reference,
558
558
  UNRESOLVED,
559
559
  None,
560
- reason=f"The source names {len(devices)} matching Devices, so no port list applies.",
560
+ reason="Resolve the source Device before choosing one of its ports.",
561
561
  )
562
562
  return None
563
- device = devices[0]
563
+ device = resolution.device
564
564
  stored = self._stored.get(_field_key(reference))
565
565
  if stored is not None:
566
566
  termination = self._stored_termination(analysis, reference, device, stored)
@@ -653,6 +653,7 @@ class _CableBatch:
653
653
  kind=_KIND_BY_MODEL_NAME[label.partition(".")[2]],
654
654
  device_id=component.device_id,
655
655
  display=str(component),
656
+ topology_display=f"{self._device_displays[component.device_id]} {component.name}",
656
657
  )
657
658
 
658
659
  def _load_mappings(self) -> None:
@@ -1197,8 +1198,8 @@ class _CableBatch:
1197
1198
  for index, stated in enumerate(stated_segments):
1198
1199
  planned = resolved.get(index)
1199
1200
  source_left, source_right = _endpoint_label(stated.left), _endpoint_label(stated.right)
1200
- left = source_left if planned is None else planned.left.display
1201
- right = source_right if planned is None else planned.right.display
1201
+ left = source_left if planned is None else planned.left.topology_display
1202
+ right = source_right if planned is None else planned.right.topology_display
1202
1203
  segments.append(
1203
1204
  {
1204
1205
  "index": index,
@@ -1222,9 +1223,11 @@ class _CableBatch:
1222
1223
  },
1223
1224
  "segments": segments,
1224
1225
  "logical_cable": self._logical_cable_display(analysis),
1226
+ "resolution_started": analysis.resolution_started,
1225
1227
  "topology_known": analysis.topology_read,
1226
1228
  # A unit with no changes proposes nothing, so the panel must not offer to delete one.
1227
1229
  "deletes_logical_cable": writes and analysis.deleted_logical_cable is not None,
1230
+ "devices": list(analysis.devices.values()),
1228
1231
  "terminations": list(analysis.terminations.values()),
1229
1232
  }
1230
1233
 
@@ -259,6 +259,7 @@ POLICY_SECTIONS: tuple[PolicySection, ...] = (
259
259
  PolicySection("source_resolutions", "Source Resolutions", _DEVICE_ONLY),
260
260
  PolicySection("device_existing_matches", "Device Existing Matches", _DEVICE_ONLY),
261
261
  PolicySection("ignored_field_differences", "Ignored Field Differences", _DEVICE_ONLY),
262
+ PolicySection("trace_device_resolutions", "Trace Device Resolutions", _TRACE_ONLY),
262
263
  PolicySection("termination_resolutions", "Termination Resolutions", _TRACE_ONLY),
263
264
  PolicySection("cable_class_mappings", "CableClass Mappings", _TRACE_ONLY),
264
265
  )
@@ -205,6 +205,7 @@ class DeviceFieldReview:
205
205
  differing: dict[str, dict[str, str]]
206
206
  ignored: dict[str, dict[str, str]]
207
207
  informational: dict[str, dict[str, str]]
208
+ matching: dict[str, dict[str, str]]
208
209
  effective_proposal: dict[str, Any]
209
210
  snapshots: dict[str, tuple[dict[str, str], dict[str, str]]]
210
211
 
@@ -272,7 +273,7 @@ class DeviceFieldReviewer:
272
273
  display_overrides: Mapping[str, str] | None = None,
273
274
  ):
274
275
  """Return the writable differences and the reported-only ones as two maps."""
275
- differing, informational, _ = DeviceFieldReviewer._compare(
276
+ differing, informational, _, _ = DeviceFieldReviewer._compare(
276
277
  matched_device,
277
278
  proposal,
278
279
  display_overrides=display_overrides,
@@ -306,7 +307,7 @@ class DeviceFieldReviewer:
306
307
  display_overrides: Mapping[str, str] | None = None,
307
308
  ) -> DeviceFieldReview:
308
309
  """Return differing, ignored, and write-safe values for one Device."""
309
- differing, informational, snapshots = self._compare(
310
+ differing, informational, matching, snapshots = self._compare(
310
311
  matched_device,
311
312
  proposal,
312
313
  display_overrides=display_overrides,
@@ -333,6 +334,7 @@ class DeviceFieldReviewer:
333
334
  differing=differing,
334
335
  ignored=ignored,
335
336
  informational=informational,
337
+ matching=matching,
336
338
  effective_proposal=effective,
337
339
  snapshots=snapshots,
338
340
  )
@@ -348,6 +350,7 @@ class DeviceFieldReviewer:
348
350
  display_overrides = display_overrides or {}
349
351
  differing: dict[str, dict[str, str]] = {}
350
352
  informational: dict[str, dict[str, str]] = {}
353
+ matching: dict[str, dict[str, str]] = {}
351
354
  snapshots: dict[str, tuple[dict[str, str], dict[str, str]]] = {}
352
355
  for definition in _FIELD_DEFINITIONS:
353
356
  if definition.target_field not in proposal or not definition.provided(proposal[definition.target_field]):
@@ -363,6 +366,11 @@ class DeviceFieldReviewer:
363
366
  if definition.target_field == "rack_name":
364
367
  _scope_rack_canonical(netbox_snapshot, _device_rack_location_id(matched_device))
365
368
  if file_snapshot["canonical"] == netbox_snapshot["canonical"]:
369
+ # Recorded, not dropped: a sync has to be able to say what it leaves alone.
370
+ matching[definition.target_field] = {
371
+ "netbox": netbox_snapshot["display"],
372
+ "file": file_snapshot["display"],
373
+ }
366
374
  continue
367
375
  snapshots[definition.target_field] = (file_snapshot, netbox_snapshot)
368
376
  values = {"netbox": netbox_snapshot["display"], "file": file_snapshot["display"]}
@@ -370,4 +378,38 @@ class DeviceFieldReviewer:
370
378
  differing[definition.target_field] = values
371
379
  else:
372
380
  informational[definition.target_field] = values
373
- return differing, informational, snapshots
381
+ return differing, informational, matching, snapshots
382
+
383
+
384
+ #: What a sync would do to one field. The preview and the sync modal read these same names.
385
+ SYNC_STATE_CHANGE = "change"
386
+ SYNC_STATE_UNCHANGED = "unchanged"
387
+ SYNC_STATE_IGNORED = "ignored"
388
+ SYNC_STATE_NOT_WRITTEN = "not_written"
389
+
390
+ #: The review bucket each state is built from, in the order the operator reads them.
391
+ _SYNC_STATE_BUCKETS: tuple[tuple[str, str], ...] = (
392
+ (SYNC_STATE_CHANGE, "field_diff"),
393
+ (SYNC_STATE_UNCHANGED, "field_matching"),
394
+ (SYNC_STATE_IGNORED, "field_ignored"),
395
+ (SYNC_STATE_NOT_WRITTEN, "field_informational"),
396
+ )
397
+
398
+
399
+ def sync_change_preview(extra_data: Mapping[str, Any], labels: Mapping[str, str]) -> list[dict[str, str]]:
400
+ """Return one matched row's reviewed fields, each labelled with what a sync would do to it."""
401
+ entries = []
402
+ for state, bucket in _SYNC_STATE_BUCKETS:
403
+ values = extra_data.get(bucket) or {}
404
+ for target_field in sorted(values):
405
+ pair = values[target_field]
406
+ entries.append(
407
+ {
408
+ "field": target_field,
409
+ "label": labels.get(target_field) or target_field,
410
+ "netbox": pair.get("netbox", ""),
411
+ "file": pair.get("file", ""),
412
+ "state": state,
413
+ }
414
+ )
415
+ return entries
@@ -3,6 +3,7 @@
3
3
  import copy
4
4
 
5
5
  from django import forms
6
+ from django.utils.html import format_html
6
7
  from dcim.models import Site, Location
7
8
  from tenancy.models import Tenant
8
9
  from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelForm, NetBoxModelImportForm
@@ -25,6 +26,24 @@ from .models import (
25
26
 
26
27
  _EXPLICIT_NONE = "__explicit_none__"
27
28
 
29
+ _CREDENTIAL_REFERENCE_EXAMPLE = """{
30
+ "backend": "vault_kv_v2",
31
+ "mount": "secret",
32
+ "path": "inference/backend",
33
+ "field": "api_key"
34
+ }"""
35
+
36
+ _CREDENTIAL_REFERENCE_HELP = format_html(
37
+ "This JSON tells the plugin where to find the API key in Vault. It does not contain the API key. "
38
+ "Configure Vault first in the NetBox plugin settings at "
39
+ "<code>PLUGINS_CONFIG[&quot;netbox_data_import&quot;][&quot;vault&quot;]</code>. "
40
+ '<details class="mt-1" data-credential-reference-help>'
41
+ "<summary>Show the required JSON shape</summary>"
42
+ '<pre class="mt-2 mb-0"><code>{}</code></pre>'
43
+ "</details>",
44
+ _CREDENTIAL_REFERENCE_EXAMPLE,
45
+ )
46
+
28
47
 
29
48
  class _RuntimeCableChoiceField(forms.ChoiceField):
30
49
  """Render only current choices while letting shared validation classify submitted values."""
@@ -132,7 +151,7 @@ class ImportProfileForm(NetBoxModelForm):
132
151
  return self.instance.source_adapter
133
152
  if self.is_bound:
134
153
  return self.data.get(self.add_prefix("source_adapter")) or self.instance.source_adapter
135
- return self.instance.source_adapter
154
+ return self.initial.get("source_adapter") or self.instance.source_adapter
136
155
 
137
156
  def clean(self):
138
157
  """Collect the adapter-declared fields into ``adapter_config``."""
@@ -306,8 +325,10 @@ class InferenceBackendForm(NetBoxModelForm):
306
325
  "tags",
307
326
  )
308
327
  help_texts = {
309
- "credential_reference": (
310
- "A typed Vault KV v2 reference: backend, mount, path and field. It never holds a key value."
328
+ "credential_reference": _CREDENTIAL_REFERENCE_HELP,
329
+ "model": (
330
+ "Enter the exact model id. After you save the backend, run the connection test to discover "
331
+ "model suggestions when the endpoint supports them."
311
332
  ),
312
333
  }
313
334
 
@@ -18,7 +18,7 @@ from django.db import DatabaseError
18
18
  from . import adapter_config, adapters, catalog, target_modules
19
19
  from .models import FailureReason, ImportExecution, SourceDocument, locked_profile_policy
20
20
  from .netbox_reader import NetBoxReader, PlanningTargetUnavailable
21
- from .object_permissions import ObjectPermissionDenied
21
+ from .object_permissions import ObjectPermissionDenied, clear_user_permission_caches
22
22
  from .plan import Diagnostic, Disposition, ImportPlan, PlanInvalid, Severity, executable_units, merge_changes
23
23
  from .source_resolution import derive_effective_rows
24
24
  from .target_runtime import DeletedObject, ExecutionContext, PreconditionFailed
@@ -225,6 +225,7 @@ class ImportEngine:
225
225
  progress_callback,
226
226
  ) -> None:
227
227
  """Compare the selection against a fresh plan and apply it, inside the caller's transaction."""
228
+ clear_user_permission_caches(actor)
228
229
  current = cls.plan(
229
230
  profile,
230
231
  source_document,
@@ -253,6 +254,7 @@ class ImportEngine:
253
254
  if runtime is None:
254
255
  raise EngineConfigurationError(f"No Target Module runtime is registered for '{change.target_module}'.")
255
256
  try:
257
+ clear_user_permission_caches(actor)
256
258
  applied = runtime.apply(change, context)
257
259
  except (PreconditionFailed, ObjectPermissionDenied, ValidationError, DatabaseError) as exc:
258
260
  raise _ExecutionFailed(