forward-netbox 2.0.0__tar.gz → 2.0.2__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 (263) hide show
  1. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/PKG-INFO +4 -2
  2. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/README.md +3 -1
  3. forward_netbox-2.0.2/forward_netbox/__init__.py +115 -0
  4. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tables.py +7 -7
  5. forward_netbox-2.0.2/forward_netbox/tests/test_device_scope_tagging.py +167 -0
  6. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_models.py +17 -0
  7. forward_netbox-2.0.2/forward_netbox/tests/test_runtime_dependency_check.py +59 -0
  8. forward_netbox-2.0.2/forward_netbox/tests/test_scope_matched_tags.py +80 -0
  9. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/apply_engine_bulk.py +2 -2
  10. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/multi_branch_lifecycle.py +5 -0
  11. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_fetch_execution.py +43 -5
  12. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync.py +3 -0
  13. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_device.py +60 -47
  14. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_interface.py +6 -0
  15. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/pyproject.toml +1 -1
  16. forward_netbox-2.0.0/forward_netbox/__init__.py +0 -83
  17. forward_netbox-2.0.0/forward_netbox/tests/test_device_scope_tagging.py +0 -121
  18. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/LICENSE +0 -0
  19. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/__init__.py +0 -0
  20. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/serializers.py +0 -0
  21. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/urls.py +0 -0
  22. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/views.py +0 -0
  23. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/choices.py +0 -0
  24. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/exceptions.py +0 -0
  25. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/filtersets.py +0 -0
  26. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/forms.py +0 -0
  27. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/jobs.py +0 -0
  28. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/__init__.py +0 -0
  29. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/__init__.py +0 -0
  30. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  31. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  32. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  33. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  34. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  35. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  36. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  37. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  38. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  39. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0001_initial.py +0 -0
  40. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  41. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  42. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  43. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  44. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  45. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  46. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  47. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  48. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  49. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  50. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  51. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  52. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  53. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  54. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  55. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  56. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  57. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  58. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  59. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  60. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  61. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  62. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  63. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  64. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  65. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  66. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  67. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/__init__.py +0 -0
  68. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/models.py +0 -0
  69. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/navigation.py +0 -0
  70. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/__init__.py +0 -0
  71. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  72. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  73. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  74. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  75. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  76. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  77. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  78. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  79. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  80. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  81. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  82. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  83. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  84. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  85. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  86. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  87. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  88. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  89. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
  90. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
  91. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  92. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_models.nqe +0 -0
  93. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  94. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_types.nqe +0 -0
  95. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  96. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_devices.nqe +0 -0
  97. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
  98. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  99. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  100. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  101. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  102. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  103. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  104. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  105. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_locations.nqe +0 -0
  106. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  107. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_modules.nqe +0 -0
  108. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  109. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  110. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  111. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  112. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_platforms.nqe +0 -0
  113. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  114. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  115. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  116. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  117. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_vlans.nqe +0 -0
  118. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_vrfs.nqe +0 -0
  119. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  120. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/signals.py +0 -0
  121. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/template_content.py +0 -0
  122. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  123. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
  124. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  125. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  126. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  127. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  128. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  129. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
  130. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  131. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  132. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  133. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
  134. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  135. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
  136. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  137. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  138. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  139. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  140. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
  141. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  142. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  143. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  144. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  145. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  146. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  147. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  148. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  149. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templatetags/__init__.py +0 -0
  150. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  151. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/__init__.py +0 -0
  152. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  153. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  154. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/scenarios.py +0 -0
  155. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_api_usage.py +0 -0
  156. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_api_views.py +0 -0
  157. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  158. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_apply_engine.py +0 -0
  159. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_branching.py +0 -0
  160. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  161. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_bulk_merge.py +0 -0
  162. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  163. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  164. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_forms.py +0 -0
  165. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_forward_api.py +0 -0
  166. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_health.py +0 -0
  167. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  168. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_interface_naming.py +0 -0
  169. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  170. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_issue_rendering.py +0 -0
  171. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_job_compat.py +0 -0
  172. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_job_liveness.py +0 -0
  173. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_jobs.py +0 -0
  174. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_log_export.py +0 -0
  175. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_logging.py +0 -0
  176. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_module_readiness.py +0 -0
  177. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  178. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_primary_ip.py +0 -0
  179. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  180. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_query_binding.py +0 -0
  181. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  182. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_query_registry.py +0 -0
  183. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  184. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_scope_module_ui.py +0 -0
  185. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sensitive_content.py +0 -0
  186. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_stability_hardening.py +0 -0
  187. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync.py +0 -0
  188. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_aci.py +0 -0
  189. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_facade.py +0 -0
  190. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  191. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  192. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_state.py +0 -0
  193. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  194. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  195. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/urls.py +0 -0
  196. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/__init__.py +0 -0
  197. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/api_usage.py +0 -0
  198. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/apply_engine.py +0 -0
  199. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  200. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/branch_budget.py +0 -0
  201. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/branching.py +0 -0
  202. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/bulk_merge.py +0 -0
  203. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/change_explainability.py +0 -0
  204. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/density_learning.py +0 -0
  205. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/device_analysis.py +0 -0
  206. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/direct_changes.py +0 -0
  207. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/drift_report.py +0 -0
  208. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/execution_ledger.py +0 -0
  209. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/execution_telemetry.py +0 -0
  210. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  211. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  212. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/forward_api.py +0 -0
  213. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/forward_api_impl.py +0 -0
  214. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health.py +0 -0
  215. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  216. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health_checks.py +0 -0
  217. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  218. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/ingestion_issues.py +0 -0
  219. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/ingestion_merge.py +0 -0
  220. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  221. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/interface_naming.py +0 -0
  222. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/job_compat.py +0 -0
  223. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/job_liveness.py +0 -0
  224. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/json_safe.py +0 -0
  225. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/logging.py +0 -0
  226. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/merge.py +0 -0
  227. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/model_contracts.py +0 -0
  228. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/model_validation.py +0 -0
  229. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/module_readiness.py +0 -0
  230. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  231. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  232. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/primary_ip.py +0 -0
  233. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_binding.py +0 -0
  234. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_binding_resolution.py +0 -0
  235. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_diagnostics.py +0 -0
  236. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_fetch.py +0 -0
  237. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_registry.py +0 -0
  238. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/runtime_guidance.py +0 -0
  239. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  240. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sensitive_content.py +0 -0
  241. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/single_branch_executor.py +0 -0
  242. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  243. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  244. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_aci.py +0 -0
  245. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_cable.py +0 -0
  246. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_contracts.py +0 -0
  247. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_core_models.py +0 -0
  248. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_events.py +0 -0
  249. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_execution.py +0 -0
  250. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_facade.py +0 -0
  251. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  252. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_ipam.py +0 -0
  253. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_orchestration.py +0 -0
  254. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_primitives.py +0 -0
  255. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_reporting.py +0 -0
  256. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_routing.py +0 -0
  257. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  258. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  259. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  260. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_state.py +0 -0
  261. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/validation.py +0 -0
  262. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/validation_org_query.py +0 -0
  263. {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forward-netbox
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: NetBox plugin to sync Forward data into NetBox via built-in NQE queries
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -31,7 +31,9 @@ Forward 26.6 is the baseline for async NQE.
31
31
 
32
32
  | Plugin Release | NetBox Version | Status |
33
33
  | --- | --- | --- |
34
- | `v2.0.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Breaking 2.0single-branch is the only execution path. Removed the per-shard branching/fast-bootstrap/resumable executor, 10k-change budget sharding, and the execution-ledger run-history; dropped the backend/max-changes/scheduler-overlap selectors |
34
+ | `v2.0.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Patch: apply_device_scope_tags now works with multiple include tags in `any` match mode each device is tagged with exactly the include tag(s) it carries (resolved per-device at fetch time), instead of skipping. Also silences the spurious `Skipping untagged VLAN 1` warning (VID 1 is NetBox's implicit access default and is intentionally not imported). No engine/schema changes; drop-in from `2.0.1`, no org republish |
35
+ | `v2.0.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.2`; Patch: fixes two 2.0.0 regressions an operator hits immediately — a false `netbox_branching is not installed; syncs will fail` startup warning (the dependency check used the wrong distribution name), and a 500 on the Sync list page (`KeyError: 'available'` from a removed execution-ledger summary). No engine or data changes; drop-in upgrade from `2.0.0` |
36
+ | `v2.0.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.1`; Breaking 2.0 — single-branch is the only execution path. Removed the per-shard branching/fast-bootstrap/resumable executor, 10k-change budget sharding, and the execution-ledger run-history; dropped the backend/max-changes/scheduler-overlap selectors |
35
37
  | `v1.7.2` | `4.6.3` required (4.5.x dropped); needs netbox-branching `1.1.0+` | Superseded by `v2.0.0`; Collection-gap diagnostics: per-reason backfill breakdown + staleness, growth/trend escalation, per-device collection result, ACI delete safety valve, opt-in auto-tag |
36
38
  | `v1.7.1` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.2`; ACI BD/L3Out graduation + FHRP churn fix (replaces yanked 1.7.0 and 1.6.2) |
37
39
  | `v1.7.0` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.1`; ACI bridge domain and L3Out NQE maps; query publish hardening |
@@ -8,7 +8,9 @@ Forward 26.6 is the baseline for async NQE.
8
8
 
9
9
  | Plugin Release | NetBox Version | Status |
10
10
  | --- | --- | --- |
11
- | `v2.0.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Breaking 2.0single-branch is the only execution path. Removed the per-shard branching/fast-bootstrap/resumable executor, 10k-change budget sharding, and the execution-ledger run-history; dropped the backend/max-changes/scheduler-overlap selectors |
11
+ | `v2.0.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Patch: apply_device_scope_tags now works with multiple include tags in `any` match mode each device is tagged with exactly the include tag(s) it carries (resolved per-device at fetch time), instead of skipping. Also silences the spurious `Skipping untagged VLAN 1` warning (VID 1 is NetBox's implicit access default and is intentionally not imported). No engine/schema changes; drop-in from `2.0.1`, no org republish |
12
+ | `v2.0.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.2`; Patch: fixes two 2.0.0 regressions an operator hits immediately — a false `netbox_branching is not installed; syncs will fail` startup warning (the dependency check used the wrong distribution name), and a 500 on the Sync list page (`KeyError: 'available'` from a removed execution-ledger summary). No engine or data changes; drop-in upgrade from `2.0.0` |
13
+ | `v2.0.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.1`; Breaking 2.0 — single-branch is the only execution path. Removed the per-shard branching/fast-bootstrap/resumable executor, 10k-change budget sharding, and the execution-ledger run-history; dropped the backend/max-changes/scheduler-overlap selectors |
12
14
  | `v1.7.2` | `4.6.3` required (4.5.x dropped); needs netbox-branching `1.1.0+` | Superseded by `v2.0.0`; Collection-gap diagnostics: per-reason backfill breakdown + staleness, growth/trend escalation, per-device collection result, ACI delete safety valve, opt-in auto-tag |
13
15
  | `v1.7.1` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.2`; ACI BD/L3Out graduation + FHRP churn fix (replaces yanked 1.7.0 and 1.6.2) |
14
16
  | `v1.7.0` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.1`; ACI bridge domain and L3Out NQE maps; query publish hardening |
@@ -0,0 +1,115 @@
1
+ try:
2
+ from netbox.plugins import PluginConfig
3
+ except ModuleNotFoundError: # pragma: no cover - tooling imports outside NetBox
4
+ PluginConfig = object
5
+
6
+
7
+ class NetboxForwardConfig(PluginConfig):
8
+ name = "forward_netbox"
9
+ verbose_name = "NetBox Forward Plugin"
10
+ description = "Sync Forward data into NetBox using built-in NQE queries."
11
+ version = "2.0.2"
12
+ base_url = "forward"
13
+ min_version = "4.6.3"
14
+
15
+ def ready(self):
16
+ super().ready()
17
+ from . import signals # noqa: F401
18
+
19
+ _check_runtime_dependencies()
20
+
21
+
22
+ def _check_runtime_dependencies():
23
+ """Warn at startup about a missing/old ``netbox_branching`` dependency.
24
+
25
+ The single-branch ingest path hard-depends on netbox-branching 1.1.0
26
+ internals (SquashMergeStrategy collapse/cycle-split/FK-graph, the
27
+ squash_dependency_graph_built signal, branch.connection_name, the
28
+ ObjectChange read router). A missing or pre-1.1.0 install otherwise only
29
+ surfaces mid-sync as an opaque failure. This logs (never raises) so NetBox
30
+ startup is unaffected.
31
+
32
+ "Installed and enabled" is tested by IMPORTABILITY, not the distribution
33
+ name: NetBox only loads the module when it is listed in PLUGINS, and the
34
+ PyPI distribution is ``netboxlabs-netbox-branching`` (not the import name
35
+ ``netbox_branching``), so keying the check off a single dist name produced a
36
+ false "not installed" warning on every boot even when it was active.
37
+ """
38
+ import logging
39
+
40
+ log = logging.getLogger("forward_netbox")
41
+ floor = (1, 1, 0)
42
+ label = "netbox_branching"
43
+
44
+ try:
45
+ import netbox_branching # noqa: F401
46
+ except Exception:
47
+ log.warning(
48
+ "forward_netbox requires the %s plugin for branch-staged syncs, but "
49
+ "it is not installed/enabled; syncs will fail. Install "
50
+ "netboxlabs-netbox-branching (>=1.1.0), add 'netbox_branching' to "
51
+ "PLUGINS in configuration.py, and run migrations.",
52
+ label,
53
+ )
54
+ return
55
+
56
+ resolved = _resolved_branching_version()
57
+ if resolved is None:
58
+ # Installed/importable but the version could not be resolved (unusual
59
+ # packaging). Do not cry wolf — the import succeeded, so syncs can run.
60
+ log.info("forward_netbox runtime dependency %s installed", label)
61
+ return
62
+
63
+ log.info("forward_netbox runtime dependency %s==%s", label, resolved)
64
+ if _version_tuple(resolved) < floor:
65
+ log.warning(
66
+ "forward_netbox requires %s>=%s but found %s; syncs will fail (the "
67
+ "plugin reuses netbox-branching 1.1.0 internals). Upgrade "
68
+ "netboxlabs-netbox-branching.",
69
+ label,
70
+ ".".join(str(part) for part in floor),
71
+ resolved,
72
+ )
73
+
74
+
75
+ def _resolved_branching_version():
76
+ """Best-effort netbox-branching version, robust to the distribution name.
77
+
78
+ The PyPI distribution is ``netboxlabs-netbox-branching``; older/forks may use
79
+ ``netbox-branching``. Fall back to the module's ``__version__``.
80
+ """
81
+ from importlib.metadata import PackageNotFoundError
82
+ from importlib.metadata import version
83
+
84
+ for dist in ("netboxlabs-netbox-branching", "netbox-branching"):
85
+ try:
86
+ return version(dist)
87
+ except PackageNotFoundError:
88
+ continue
89
+ except Exception: # pragma: no cover - never block startup
90
+ return None
91
+ try:
92
+ import netbox_branching
93
+
94
+ return getattr(netbox_branching, "__version__", None)
95
+ except Exception: # pragma: no cover - never block startup
96
+ return None
97
+
98
+
99
+ def _version_tuple(value):
100
+ """Best-effort (major, minor, patch) tuple from a version string."""
101
+ parts = []
102
+ for chunk in str(value).split(".")[:3]:
103
+ digits = ""
104
+ for char in chunk:
105
+ if char.isdigit():
106
+ digits += char
107
+ else:
108
+ break
109
+ parts.append(int(digits) if digits else 0)
110
+ while len(parts) < 3:
111
+ parts.append(0)
112
+ return tuple(parts)
113
+
114
+
115
+ config = NetboxForwardConfig
@@ -16,8 +16,6 @@ from .models import ForwardNQEMap
16
16
  from .models import ForwardSource
17
17
  from .models import ForwardSync
18
18
  from .models import ForwardValidationRun
19
- from .utilities.execution_ledger import execution_run_failure_summary
20
- from .utilities.execution_ledger import latest_execution_run
21
19
  from .utilities.json_safe import json_safe_value
22
20
 
23
21
 
@@ -90,13 +88,15 @@ class ForwardSyncTable(NetBoxTable):
90
88
  return getattr(value, "name", "---") if value else "---"
91
89
 
92
90
  def render_latest_failure(self, value, record):
93
- summary = execution_run_failure_summary(latest_execution_run(record))
94
- if not summary["available"]:
91
+ # 2.0 removed the execution-run ledger; surface the latest ingestion's
92
+ # failed-change count (the always-populated 2.0 failure signal) instead.
93
+ ingestion = record.forwardingestion_set.last()
94
+ failed = getattr(ingestion, "failed_change_count", 0) or 0
95
+ if not failed:
95
96
  return "---"
96
97
  return format_html(
97
- '<span title="{}">{}</span>',
98
- summary["error"],
99
- summary["message"],
98
+ '<span class="text-danger">{}</span>',
99
+ _("{n} failed change(s)").format(n=failed),
100
100
  )
101
101
 
102
102
  class Meta(NetBoxTable.Meta):
@@ -0,0 +1,167 @@
1
+ from unittest.mock import Mock
2
+
3
+ from dcim.models import Device
4
+ from django.test import TestCase
5
+ from extras.models import Tag
6
+
7
+ from forward_netbox.models import ForwardSource
8
+ from forward_netbox.models import ForwardSync
9
+ from forward_netbox.utilities.sync import ForwardSyncRunner
10
+ from forward_netbox.utilities.sync_device import apply_dcim_device
11
+
12
+
13
+ class DeviceScopeTaggingTest(TestCase):
14
+ """apply_device_scope_tags tags each device with exactly the include tags it
15
+ carries. The per-device matched-tag map is resolved at fetch time and set on
16
+ the runner (runner._scope_matched_tags) by the executor."""
17
+
18
+ def _source(
19
+ self, *, apply_scope_tags, include_tags=("N.Patel",), include_match="any"
20
+ ):
21
+ return ForwardSource.objects.create(
22
+ name=f"scope-src-{ForwardSource.objects.count()}",
23
+ type="saas",
24
+ url="https://fwd.app",
25
+ status="ready",
26
+ parameters={
27
+ "username": "u@example.com",
28
+ "password": "p",
29
+ "verify": True,
30
+ "network_id": "net-1",
31
+ "device_tag_include_tags": list(include_tags),
32
+ "device_tag_include_match": include_match,
33
+ "apply_device_scope_tags": apply_scope_tags,
34
+ },
35
+ )
36
+
37
+ def _sync(self, source):
38
+ return ForwardSync.objects.create(
39
+ name=f"scope-sync-{source.pk}",
40
+ source=source,
41
+ parameters={"snapshot_id": "latestProcessed"},
42
+ )
43
+
44
+ def _runner(self, sync, matched=None):
45
+ runner = ForwardSyncRunner(
46
+ sync=sync, ingestion=None, client=None, logger_=Mock()
47
+ )
48
+ runner._scope_matched_tags = dict(matched or {})
49
+ return runner
50
+
51
+ def _row(self, name="dev-1"):
52
+ return {
53
+ "name": name,
54
+ "site": "site-a",
55
+ "site_slug": "site-a",
56
+ "role": "role-a",
57
+ "role_slug": "role-a",
58
+ "role_color": "9e9e9e",
59
+ "manufacturer": "Cisco",
60
+ "manufacturer_slug": "cisco",
61
+ "device_type": "model-a",
62
+ "device_type_slug": "model-a",
63
+ "platform": "",
64
+ "platform_slug": "",
65
+ "status": "active",
66
+ }
67
+
68
+ def test_scope_tag_applied_when_enabled(self):
69
+ sync = self._sync(self._source(apply_scope_tags=True))
70
+ runner = self._runner(sync, matched={"dev-1": ["N.Patel"]})
71
+ apply_dcim_device(runner, self._row())
72
+
73
+ device = Device.objects.get(name="dev-1")
74
+ self.assertEqual(
75
+ sorted(device.tags.values_list("name", flat=True)), ["N.Patel"]
76
+ )
77
+ self.assertEqual(Tag.objects.filter(name="N.Patel").count(), 1)
78
+
79
+ def test_scope_tag_not_applied_when_disabled(self):
80
+ sync = self._sync(self._source(apply_scope_tags=False))
81
+ runner = self._runner(sync, matched={"dev-1": ["N.Patel"]})
82
+ apply_dcim_device(runner, self._row())
83
+
84
+ device = Device.objects.get(name="dev-1")
85
+ self.assertEqual(list(device.tags.all()), [])
86
+ self.assertFalse(Tag.objects.filter(name="N.Patel").exists())
87
+
88
+ def test_reapply_does_not_duplicate_tag(self):
89
+ sync = self._sync(self._source(apply_scope_tags=True))
90
+ apply_dcim_device(self._runner(sync, {"dev-1": ["N.Patel"]}), self._row())
91
+ # Fresh runner (cleared caches) re-applies the same device -> idempotent.
92
+ apply_dcim_device(self._runner(sync, {"dev-1": ["N.Patel"]}), self._row())
93
+
94
+ device = Device.objects.get(name="dev-1")
95
+ self.assertEqual(device.tags.filter(name="N.Patel").count(), 1)
96
+
97
+ def test_multi_tag_any_applies_only_carried_tags(self):
98
+ # The previously-skipped case: multiple include tags, "any" mode. The
99
+ # device carries only TagA, so it gets only TagA — never TagB.
100
+ sync = self._sync(
101
+ self._source(
102
+ apply_scope_tags=True,
103
+ include_tags=["TagA", "TagB"],
104
+ include_match="any",
105
+ )
106
+ )
107
+ runner = self._runner(sync, matched={"dev-1": ["TagA"]})
108
+ apply_dcim_device(runner, self._row())
109
+
110
+ device = Device.objects.get(name="dev-1")
111
+ self.assertEqual(sorted(device.tags.values_list("name", flat=True)), ["TagA"])
112
+ self.assertFalse(device.tags.filter(name="TagB").exists())
113
+
114
+ def test_multi_tag_all_mode_applies_all(self):
115
+ # "all" mode: every in-scope device carries every include tag (the
116
+ # resolver yields the full set), so all are applied — identical to before.
117
+ sync = self._sync(
118
+ self._source(
119
+ apply_scope_tags=True,
120
+ include_tags=["TagA", "TagB"],
121
+ include_match="all",
122
+ )
123
+ )
124
+ runner = self._runner(sync, matched={"dev-1": ["TagA", "TagB"]})
125
+ apply_dcim_device(runner, self._row())
126
+
127
+ device = Device.objects.get(name="dev-1")
128
+ self.assertEqual(
129
+ sorted(device.tags.values_list("name", flat=True)), ["TagA", "TagB"]
130
+ )
131
+
132
+ def test_stale_scope_tag_removed_user_tag_preserved(self):
133
+ # A device that drops a Forward include tag between syncs loses the
134
+ # corresponding scope tag, but unrelated (user/feature) tags are kept.
135
+ sync = self._sync(
136
+ self._source(
137
+ apply_scope_tags=True,
138
+ include_tags=["TagA", "TagB"],
139
+ include_match="any",
140
+ )
141
+ )
142
+ apply_dcim_device(self._runner(sync, {"dev-1": ["TagA", "TagB"]}), self._row())
143
+ device = Device.objects.get(name="dev-1")
144
+ user_tag, _ = Tag.objects.get_or_create(name="owner-x", slug="owner-x")
145
+ device.tags.add(user_tag)
146
+ self.assertEqual(
147
+ sorted(device.tags.values_list("name", flat=True)),
148
+ ["TagA", "TagB", "owner-x"],
149
+ )
150
+
151
+ # Re-sync: device now carries only TagA.
152
+ apply_dcim_device(self._runner(sync, {"dev-1": ["TagA"]}), self._row())
153
+ device.refresh_from_db()
154
+ self.assertEqual(
155
+ sorted(device.tags.values_list("name", flat=True)),
156
+ ["TagA", "owner-x"],
157
+ )
158
+
159
+ def test_unmatched_device_gets_no_scope_tags(self):
160
+ # A device absent from the matched map (e.g. exclude-only scope) is left
161
+ # untagged and does not crash.
162
+ sync = self._sync(self._source(apply_scope_tags=True))
163
+ runner = self._runner(sync, matched={})
164
+ apply_dcim_device(runner, self._row())
165
+
166
+ device = Device.objects.get(name="dev-1")
167
+ self.assertEqual(list(device.tags.all()), [])
@@ -1591,6 +1591,23 @@ class ForwardSyncModelTest(TestCase):
1591
1591
  def test_sync_table_shows_scheduled_by_default(self):
1592
1592
  self.assertIn("scheduled", ForwardSyncTable.Meta.default_columns)
1593
1593
 
1594
+ def test_sync_table_latest_failure_renders_without_ledger(self):
1595
+ # Regression: render_latest_failure called the removed execution-ledger
1596
+ # stubs and indexed summary["available"] -> KeyError, 500'ing the sync
1597
+ # list page in 2.0. It must render from the ingestion's failed-change
1598
+ # count and never crash.
1599
+ from forward_netbox.models import ForwardIngestion
1600
+
1601
+ sync = ForwardSync.objects.create(name="lf-sync", source=self.source)
1602
+ table = ForwardSyncTable(ForwardSync.objects.filter(pk=sync.pk))
1603
+ # No ingestion -> graceful dash.
1604
+ self.assertEqual(table.render_latest_failure(value=None, record=sync), "---")
1605
+ # Failed changes -> surfaced count, still no crash.
1606
+ ForwardIngestion.objects.create(sync=sync, failed_change_count=3)
1607
+ rendered = str(table.render_latest_failure(value=None, record=sync))
1608
+ self.assertIn("3", rendered)
1609
+ self.assertNotIn("available", rendered)
1610
+
1594
1611
  @patch("forward_netbox.models.ForwardSource.get_client")
1595
1612
  @patch(
1596
1613
  "forward_netbox.utilities.single_branch_executor.ForwardSingleBranchExecutor"
@@ -0,0 +1,59 @@
1
+ import logging
2
+ from unittest.mock import patch
3
+
4
+ from django.test import SimpleTestCase
5
+
6
+ from forward_netbox import _check_runtime_dependencies
7
+ from forward_netbox import _resolved_branching_version
8
+
9
+
10
+ class RuntimeDependencyCheckTest(SimpleTestCase):
11
+ """Guards the netbox_branching startup dependency check.
12
+
13
+ Regression: the check keyed off the distribution name ``netbox-branching``,
14
+ but the PyPI distribution is ``netboxlabs-netbox-branching`` — so
15
+ importlib.metadata.version() always raised PackageNotFoundError and the
16
+ plugin logged a false "netbox_branching is not installed; syncs will fail"
17
+ warning on EVERY boot, even when branching was installed and active.
18
+ """
19
+
20
+ def _capture(self):
21
+ logger = logging.getLogger("forward_netbox")
22
+ with self.assertLogs(logger, level="INFO") as cm:
23
+ _check_runtime_dependencies()
24
+ return "\n".join(cm.output)
25
+
26
+ def test_resolves_version_by_correct_distribution_name(self):
27
+ # netbox-branching is installed as the netboxlabs-netbox-branching dist.
28
+ self.assertIsNotNone(_resolved_branching_version())
29
+
30
+ def test_no_false_not_installed_warning_when_installed(self):
31
+ out = self._capture()
32
+ self.assertNotIn("not installed", out)
33
+ self.assertNotIn("syncs will fail", out)
34
+
35
+ def test_warns_when_below_floor(self):
36
+ with patch("forward_netbox._resolved_branching_version", return_value="1.0.4"):
37
+ out = self._capture()
38
+ self.assertIn("netbox_branching>=1.1.0", out)
39
+ self.assertIn("syncs will fail", out)
40
+
41
+ def test_warns_when_not_importable(self):
42
+ real_import = (
43
+ __builtins__["__import__"]
44
+ if isinstance(__builtins__, dict)
45
+ else __builtins__.__import__
46
+ )
47
+
48
+ def fake_import(name, *args, **kwargs):
49
+ if name == "netbox_branching":
50
+ raise ImportError("simulated missing plugin")
51
+ return real_import(name, *args, **kwargs)
52
+
53
+ logger = logging.getLogger("forward_netbox")
54
+ with patch("builtins.__import__", side_effect=fake_import):
55
+ with self.assertLogs(logger, level="WARNING") as cm:
56
+ _check_runtime_dependencies()
57
+ out = "\n".join(cm.output)
58
+ self.assertIn("not installed/enabled", out)
59
+ self.assertIn("PLUGINS", out)
@@ -0,0 +1,80 @@
1
+ from unittest.mock import Mock
2
+
3
+ from django.test import TestCase
4
+
5
+ from forward_netbox.models import ForwardSource
6
+ from forward_netbox.models import ForwardSync
7
+ from forward_netbox.utilities.query_fetch import ForwardQueryContext
8
+ from forward_netbox.utilities.query_fetch_execution import ForwardQueryFetcher
9
+
10
+
11
+ class ScopeMatchedTagsResolveTest(TestCase):
12
+ """The inline scope resolver returns, per in-scope device, exactly the
13
+ include tags that device carries (the apply_device_scope_tags per-device map),
14
+ intersected at resolve time so the persisted payload stays small."""
15
+
16
+ def _fetcher(self, client):
17
+ source = ForwardSource.objects.create(
18
+ name=f"smt-src-{ForwardSource.objects.count()}",
19
+ type="saas",
20
+ url="https://fwd.app",
21
+ status="ready",
22
+ parameters={
23
+ "username": "u",
24
+ "password": "p",
25
+ "verify": True,
26
+ "network_id": "net-1",
27
+ },
28
+ )
29
+ sync = ForwardSync.objects.create(
30
+ name=f"smt-sync-{source.pk}",
31
+ source=source,
32
+ parameters={"snapshot_id": "latestProcessed"},
33
+ )
34
+ return ForwardQueryFetcher(sync=sync, client=client, logger_=Mock())
35
+
36
+ def test_resolve_returns_per_device_intersection(self):
37
+ client = Mock()
38
+ client.run_nqe_query.return_value = [
39
+ {"name": "d1", "site": "s", "tagNames": ["TagA", "TagX"]},
40
+ {"name": "d2", "site": "s", "tagNames": ["TagB"]},
41
+ {"name": "d3", "site": "s", "tagNames": ["TagA", "TagB"]},
42
+ {"name": "d4", "site": "s", "tagNames": ["TagX"]},
43
+ ]
44
+ names, sites, matched = self._fetcher(client)._resolve_scoped_tag_scope(
45
+ network_id="net-1",
46
+ snapshot_id="snap",
47
+ include_tags=["TagA", "TagB"],
48
+ exclude_tags=[],
49
+ include_match="any",
50
+ )
51
+ self.assertEqual(
52
+ matched, {"d1": ["TagA"], "d2": ["TagB"], "d3": ["TagA", "TagB"]}
53
+ )
54
+ # d4 carries none of the include tags -> not in the map (no tagging).
55
+ self.assertNotIn("d4", matched)
56
+ # The query must request tagNames.
57
+ query = client.run_nqe_query.call_args.kwargs["query"]
58
+ self.assertIn("tagNames: device.tagNames", query)
59
+
60
+ def test_resolve_no_tags_returns_empty_map(self):
61
+ result = self._fetcher(Mock())._resolve_scoped_tag_scope(
62
+ network_id="net-1",
63
+ snapshot_id="snap",
64
+ include_tags=[],
65
+ exclude_tags=[],
66
+ include_match="any",
67
+ )
68
+ self.assertEqual(result, (set(), set(), {}))
69
+
70
+ def test_context_as_dict_carries_full_matched_map(self):
71
+ ctx = ForwardQueryContext(
72
+ network_id="net-1",
73
+ snapshot_selector="latestProcessed",
74
+ snapshot_id="snap",
75
+ scoped_matched_tags={"d1": ["TagA"], "d2": ["TagB"]},
76
+ )
77
+ # The branch apply path reads this back — it must be the full map, not a count.
78
+ self.assertEqual(
79
+ ctx.as_dict()["scoped_matched_tags"], {"d1": ["TagA"], "d2": ["TagB"]}
80
+ )
@@ -1105,7 +1105,7 @@ def bulk_orm_apply_device(runner, rows: list[dict[str, Any]]):
1105
1105
 
1106
1106
  from ..exceptions import ForwardDependencySkipError
1107
1107
  from ..exceptions import ForwardSearchError
1108
- from .sync_device import _scope_tags
1108
+ from .sync_device import _scope_tags_enabled
1109
1109
  from .sync_device import apply_dcim_device
1110
1110
 
1111
1111
  update_field_names = ["site", "role", "device_type", "platform", "serial", "status"]
@@ -1138,7 +1138,7 @@ def bulk_orm_apply_device(runner, rows: list[dict[str, Any]]):
1138
1138
 
1139
1139
  # Opt-in per-device scope tagging is an adapter side effect; keep exact parity
1140
1140
  # by delegating the whole batch when it is enabled.
1141
- if _scope_tags(runner):
1141
+ if _scope_tags_enabled(runner):
1142
1142
  for row in rows:
1143
1143
  _delegate(row)
1144
1144
  runner.events_clearer.clear()
@@ -85,6 +85,11 @@ def run_item_in_branch(executor, item, context, ingestion, branch, *, total_plan
85
85
  logger_=executor.logger,
86
86
  )
87
87
  runner._model_coalesce_fields[item.model_string] = item.coalesce_fields
88
+ # Per-device scope-tag map resolved at fetch time (apply_device_scope_tags);
89
+ # without this, branched syncs would tag nothing.
90
+ runner._scope_matched_tags = {
91
+ str(k): list(v) for k, v in (context.get("scoped_matched_tags") or {}).items()
92
+ }
88
93
  ingestion.snapshot_selector = context["snapshot_selector"]
89
94
  ingestion.snapshot_id = context["snapshot_id"]
90
95
  ingestion.snapshot_info = context["snapshot_info"]