forward-netbox 2.2.0__tar.gz → 2.2.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 (280) hide show
  1. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/PKG-INFO +4 -2
  2. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/README.md +3 -1
  3. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/__init__.py +1 -1
  4. forward_netbox-2.2.2/forward_netbox/management/commands/forward_apply_identity_audit.py +80 -0
  5. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardingestion.html +1 -1
  6. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +1 -1
  7. forward_netbox-2.2.2/forward_netbox/tests/test_apply_identity_audit.py +50 -0
  8. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_log_export.py +60 -0
  9. forward_netbox-2.2.2/forward_netbox/utilities/apply_identity_audit.py +173 -0
  10. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/views.py +21 -7
  11. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/pyproject.toml +1 -1
  12. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/LICENSE +0 -0
  13. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/api/__init__.py +0 -0
  14. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/api/serializers.py +0 -0
  15. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/api/urls.py +0 -0
  16. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/api/views.py +0 -0
  17. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/choices.py +0 -0
  18. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/exceptions.py +0 -0
  19. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/filtersets.py +0 -0
  20. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/forms.py +0 -0
  21. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/jobs.py +0 -0
  22. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/__init__.py +0 -0
  23. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/__init__.py +0 -0
  24. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  25. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  26. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  27. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  28. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
  29. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  30. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
  31. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  32. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  33. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
  34. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  35. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
  36. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  37. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0001_initial.py +0 -0
  38. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  39. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  40. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  41. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  42. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  43. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  44. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  45. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  46. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  47. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  48. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  49. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  50. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  51. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  52. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  53. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  54. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  55. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  56. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  57. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  58. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  59. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  60. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  61. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  62. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  63. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  64. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  65. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
  66. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/migrations/__init__.py +0 -0
  67. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/models.py +0 -0
  68. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/navigation.py +0 -0
  69. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/__init__.py +0 -0
  70. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  71. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  72. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  73. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  74. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  75. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  76. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  77. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  78. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  79. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  80. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  81. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  82. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  83. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  84. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  85. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  86. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  87. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  88. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
  89. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
  90. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  91. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_models.nqe +0 -0
  92. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  93. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_types.nqe +0 -0
  94. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  95. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_devices.nqe +0 -0
  96. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
  97. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  98. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  99. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  100. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  101. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  102. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  103. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  104. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_locations.nqe +0 -0
  105. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  106. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_modules.nqe +0 -0
  107. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  108. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  109. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  110. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  111. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_platforms.nqe +0 -0
  112. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  113. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  114. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  115. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  116. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_vlans.nqe +0 -0
  117. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/forward_vrfs.nqe +0 -0
  118. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  119. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/signals.py +0 -0
  120. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
  121. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tables.py +0 -0
  122. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/template_content.py +0 -0
  123. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  124. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  125. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  126. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  127. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  128. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  129. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
  130. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  131. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  132. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  133. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
  134. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
  135. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  136. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
  137. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  138. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  139. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  140. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  141. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  142. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  143. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  144. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  145. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  146. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  147. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  148. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  149. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templatetags/__init__.py +0 -0
  150. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  151. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/__init__.py +0 -0
  152. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  153. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  154. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/scenarios.py +0 -0
  155. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_api_usage.py +0 -0
  156. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_api_views.py +0 -0
  157. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  158. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_apply_engine.py +0 -0
  159. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_branching.py +0 -0
  160. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_brand_bar.py +0 -0
  161. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  162. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_bulk_merge.py +0 -0
  163. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  164. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  165. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
  166. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_forms.py +0 -0
  167. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_forward_api.py +0 -0
  168. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_health.py +0 -0
  169. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  170. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_interface_naming.py +0 -0
  171. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
  172. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  173. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_issue_rendering.py +0 -0
  174. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_job_compat.py +0 -0
  175. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_job_liveness.py +0 -0
  176. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_jobs.py +0 -0
  177. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_logging.py +0 -0
  178. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_models.py +0 -0
  179. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_module_readiness.py +0 -0
  180. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
  181. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  182. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_primary_ip.py +0 -0
  183. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
  184. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  185. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_query_binding.py +0 -0
  186. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  187. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_query_registry.py +0 -0
  188. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  189. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
  190. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
  191. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
  192. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_scope_module_ui.py +0 -0
  193. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sensitive_content.py +0 -0
  194. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_stability_hardening.py +0 -0
  195. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sync.py +0 -0
  196. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sync_aci.py +0 -0
  197. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sync_facade.py +0 -0
  198. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  199. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  200. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_sync_state.py +0 -0
  201. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  202. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  203. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/tests/test_vsys_parent.py +0 -0
  204. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/urls.py +0 -0
  205. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/__init__.py +0 -0
  206. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/api_usage.py +0 -0
  207. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/apply_engine.py +0 -0
  208. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
  209. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  210. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/branch_budget.py +0 -0
  211. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/branching.py +0 -0
  212. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/bulk_merge.py +0 -0
  213. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/change_explainability.py +0 -0
  214. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/density_learning.py +0 -0
  215. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/device_analysis.py +0 -0
  216. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/direct_changes.py +0 -0
  217. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/drift_report.py +0 -0
  218. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/execution_ledger.py +0 -0
  219. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/execution_telemetry.py +0 -0
  220. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  221. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  222. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/forward_api.py +0 -0
  223. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/forward_api_impl.py +0 -0
  224. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/health.py +0 -0
  225. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  226. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/health_checks.py +0 -0
  227. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  228. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/ingestion_issues.py +0 -0
  229. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/ingestion_merge.py +0 -0
  230. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  231. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/interface_naming.py +0 -0
  232. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/job_compat.py +0 -0
  233. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/job_liveness.py +0 -0
  234. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/json_safe.py +0 -0
  235. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/logging.py +0 -0
  236. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/merge.py +0 -0
  237. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/model_contracts.py +0 -0
  238. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/model_validation.py +0 -0
  239. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/module_readiness.py +0 -0
  240. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
  241. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  242. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  243. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/primary_ip.py +0 -0
  244. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/primary_ip_audit.py +0 -0
  245. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/query_binding.py +0 -0
  246. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/query_binding_resolution.py +0 -0
  247. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/query_diagnostics.py +0 -0
  248. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/query_fetch.py +0 -0
  249. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/query_fetch_execution.py +0 -0
  250. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/query_registry.py +0 -0
  251. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/runtime_guidance.py +0 -0
  252. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
  253. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  254. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sensitive_content.py +0 -0
  255. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/single_branch_executor.py +0 -0
  256. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  257. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  258. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync.py +0 -0
  259. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_aci.py +0 -0
  260. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_cable.py +0 -0
  261. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_contracts.py +0 -0
  262. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_core_models.py +0 -0
  263. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_device.py +0 -0
  264. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_events.py +0 -0
  265. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_execution.py +0 -0
  266. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_facade.py +0 -0
  267. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_interface.py +0 -0
  268. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  269. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_ipam.py +0 -0
  270. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_orchestration.py +0 -0
  271. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_primitives.py +0 -0
  272. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_reporting.py +0 -0
  273. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_routing.py +0 -0
  274. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  275. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  276. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  277. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/sync_state.py +0 -0
  278. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/validation.py +0 -0
  279. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/validation_org_query.py +0 -0
  280. {forward_netbox-2.2.0 → forward_netbox-2.2.2}/forward_netbox/utilities/vsys_parent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forward-netbox
3
- Version: 2.2.0
3
+ Version: 2.2.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.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
34
+ | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
35
+ | `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
36
+ | `v2.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.1`; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
35
37
  | `v2.1.5` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.0`; Fix Prune orphans erroring on empty sites that still hold a VLAN/VM/prefix (delete only truly-empty sites) |
36
38
  | `v2.1.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.1.5`; Tag delete-eligible global IPAM (prefixes/VLANs/VRFs) for manual review |
37
39
  | `v2.1.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.1.4`; Prune empty orphan sites (zero devices + zero racks) alongside out-of-scope devices |
@@ -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.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
11
+ | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
12
+ | `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
13
+ | `v2.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.1`; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
12
14
  | `v2.1.5` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.0`; Fix Prune orphans erroring on empty sites that still hold a VLAN/VM/prefix (delete only truly-empty sites) |
13
15
  | `v2.1.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.1.5`; Tag delete-eligible global IPAM (prefixes/VLANs/VRFs) for manual review |
14
16
  | `v2.1.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.1.4`; Prune empty orphan sites (zero devices + zero racks) alongside out-of-scope devices |
@@ -8,7 +8,7 @@ class NetboxForwardConfig(PluginConfig):
8
8
  name = "forward_netbox"
9
9
  verbose_name = "Forward Field Integration"
10
10
  description = "Sync Forward data into NetBox using built-in NQE queries."
11
- version = "2.2.0"
11
+ version = "2.2.2"
12
12
  base_url = "forward"
13
13
  min_version = "4.6.3"
14
14
 
@@ -0,0 +1,80 @@
1
+ import json
2
+
3
+ from django.core.management.base import BaseCommand
4
+ from django.core.management.base import CommandError
5
+
6
+ from forward_netbox.models import ForwardSync
7
+ from forward_netbox.utilities.apply_identity_audit import audit_apply_identity
8
+ from forward_netbox.utilities.apply_identity_audit import SIMPLE_MODELS
9
+
10
+
11
+ class Command(BaseCommand):
12
+ help = (
13
+ "Read-only diagnostic for the '1 created + 1 deleted every sync' churn. "
14
+ "For each simple-dimension model it compares Forward-computed natural keys "
15
+ "against NetBox-stored keys and reports the would-create / would-delete "
16
+ "leftovers. A model flagged churn_suspect (both non-empty, often 1/1) "
17
+ "names the object whose key Forward and NetBox disagree on. Never writes."
18
+ )
19
+
20
+ def add_arguments(self, parser):
21
+ parser.add_argument("--sync-id", type=int, default=0)
22
+ parser.add_argument("--sync-name", default="")
23
+ parser.add_argument(
24
+ "--models",
25
+ default="",
26
+ help=(
27
+ "Comma-separated subset of "
28
+ + ",".join(SIMPLE_MODELS)
29
+ + " (default: all enabled)."
30
+ ),
31
+ )
32
+ parser.add_argument("--limit", type=int, default=15, help="Sample size.")
33
+
34
+ def handle(self, *args, **options):
35
+ if options["sync_id"] and options["sync_name"]:
36
+ raise CommandError("Use either --sync-id or --sync-name, not both.")
37
+ sync = self._resolve_sync(options)
38
+ if sync is None:
39
+ raise CommandError("No sync found for the requested selector.")
40
+ if not sync.get_network_id():
41
+ raise CommandError("Sync source has no network configured.")
42
+
43
+ requested = [m.strip() for m in options["models"].split(",") if m.strip()]
44
+ unsupported = [m for m in requested if m not in SIMPLE_MODELS]
45
+ if unsupported:
46
+ raise CommandError(
47
+ "Unsupported --models: "
48
+ + ", ".join(unsupported)
49
+ + ". Supported: "
50
+ + ", ".join(SIMPLE_MODELS)
51
+ + "."
52
+ )
53
+
54
+ payload = audit_apply_identity(
55
+ sync,
56
+ models=requested or None,
57
+ sample_limit=int(options["limit"] or 15),
58
+ )
59
+ payload["remediation"] = (
60
+ ""
61
+ if not payload["churn_suspect_models"]
62
+ else (
63
+ "Churn suspected in: "
64
+ + ", ".join(payload["churn_suspect_models"])
65
+ + ". Compare each model's would_create_sample vs "
66
+ "would_delete_sample — they are the same object with a differing "
67
+ "key (e.g. a slug/name that Forward and NetBox compute "
68
+ "differently). That key difference is the bug to fix."
69
+ )
70
+ )
71
+ self.stdout.write(json.dumps(payload, indent=2, default=str))
72
+
73
+ def _resolve_sync(self, options):
74
+ sync_id = int(options.get("sync_id") or 0)
75
+ sync_name = (options.get("sync_name") or "").strip()
76
+ if sync_id:
77
+ return ForwardSync.objects.filter(pk=sync_id).first()
78
+ if sync_name:
79
+ return ForwardSync.objects.filter(name=sync_name).first()
80
+ return ForwardSync.objects.order_by("-id").first()
@@ -167,7 +167,7 @@
167
167
  <div id="ingestion_poll"
168
168
  class="col col-md-12"
169
169
  hx-get="{% url 'plugins:forward_netbox:forwardingestion_logs' pk=object.pk %}?stage={{ active_stage|default:'sync' }}"
170
- hx-trigger="{% if job_running %}every 5s{% else %}none{% endif %}"
170
+ hx-trigger="{% if job_running %}every 15s{% else %}none{% endif %}"
171
171
  hx-target="#ingestion_logs"
172
172
  hx-select="#ingestion_logs"
173
173
  hx-select-oob="#ingestion_status:innerHTML,#ingestion_progress:innerHTML,#ingestion_statistics:innerHTML,#change_explainability:innerHTML,#object_tabs:innerHTML,#ingestion_merge_button:innerHTML"
@@ -22,7 +22,7 @@
22
22
  hx-swap-oob="true"
23
23
  class="col col-md-12"
24
24
  hx-get="{{ logs_url }}?stage={{ active_stage }}"
25
- hx-trigger="{% if polling_done %}none{% else %}every 5s{% endif %}"
25
+ hx-trigger="{% if polling_done %}none{% else %}every 15s{% endif %}"
26
26
  hx-target="#ingestion_logs"
27
27
  hx-select="#ingestion_logs"
28
28
  hx-select-oob="#ingestion_status:innerHTML,#ingestion_progress:innerHTML,#ingestion_statistics:innerHTML,#change_explainability:innerHTML,#object_tabs:innerHTML,#ingestion_merge_button:innerHTML"
@@ -0,0 +1,50 @@
1
+ from types import SimpleNamespace
2
+
3
+ from dcim.models import Site
4
+ from django.test import TestCase
5
+
6
+ from forward_netbox.utilities.apply_identity_audit import audit_apply_identity
7
+ from forward_netbox.utilities.apply_identity_audit import audit_model_identity
8
+
9
+
10
+ class ApplyIdentityAuditTest(TestCase):
11
+ def test_clean_when_keys_match(self):
12
+ Site.objects.create(name="Keep", slug="keep")
13
+ rows = [{"slug": "keep", "name": "Keep"}]
14
+ result = audit_model_identity("dcim.site", rows)
15
+ self.assertEqual(result["would_create_count"], 0)
16
+ self.assertEqual(result["would_delete_count"], 0)
17
+ self.assertFalse(result["churn_suspect"])
18
+
19
+ def test_slug_mismatch_is_flagged_as_churn(self):
20
+ # Same logical site, but NetBox stored a different slug/name than Forward
21
+ # now computes -> the apply engine would delete the orphan and create the
22
+ # new one every sync. This is the 1-created/1-deleted signature.
23
+ Site.objects.create(name="Old Name", slug="old-slug")
24
+ rows = [{"slug": "new-slug", "name": "New Name"}]
25
+ result = audit_model_identity("dcim.site", rows)
26
+ self.assertEqual(result["would_create_count"], 1)
27
+ self.assertEqual(result["would_delete_count"], 1)
28
+ self.assertTrue(result["churn_suspect"])
29
+ self.assertIn("old-slug", result["would_delete_sample"][0])
30
+ self.assertIn("new-slug", result["would_create_sample"][0])
31
+
32
+ def test_name_set_match_is_not_false_churn(self):
33
+ # Forward slug differs but the NAME still matches -> the apply engine
34
+ # matches on the name lookup_set, so it is NOT churn.
35
+ Site.objects.create(name="Shared", slug="stored-slug")
36
+ rows = [{"slug": "different-slug", "name": "Shared"}]
37
+ result = audit_model_identity("dcim.site", rows)
38
+ self.assertEqual(result["would_delete_count"], 0)
39
+ self.assertEqual(result["would_create_count"], 0)
40
+ self.assertFalse(result["churn_suspect"])
41
+
42
+ def test_audit_filters_to_enabled_models(self):
43
+ Site.objects.create(name="A", slug="a")
44
+ sync = SimpleNamespace(get_model_strings=lambda: ["dcim.site", "dcim.device"])
45
+ canned = {"dcim.site": [{"slug": "a", "name": "A"}]}
46
+ payload = audit_apply_identity(
47
+ sync, fetch_rows=lambda s, model: canned.get(model, [])
48
+ )
49
+ self.assertEqual(payload["models_audited"], ["dcim.site"])
50
+ self.assertEqual(payload["churn_suspect_models"], [])
@@ -338,6 +338,66 @@ class ForwardIngestionLogExportViewTest(TestCase):
338
338
  self.assertContains(response, "ipam.prefix 1")
339
339
  self.assertContains(response, "vrf 1")
340
340
 
341
+ def test_poll_defers_change_explainability_while_running(self):
342
+ # Regression (504 fix): while the job is running, the 5s/15s poll must
343
+ # NOT recompute change_explainability — doing so on every poll piles DB
344
+ # load onto the web workers during a long settling merge, which is what
345
+ # produces the gateway 504s on large platform-reclassification syncs.
346
+ running_sync = ForwardSync.objects.create(
347
+ name="sync-running-poll",
348
+ source=self.source,
349
+ parameters={"snapshot_id": "latestProcessed"},
350
+ )
351
+ running_ing = ForwardIngestion.objects.create(
352
+ sync=running_sync,
353
+ snapshot_selector="latestProcessed",
354
+ snapshot_id="snap-running",
355
+ )
356
+ now = timezone.now()
357
+ running_job = Job.objects.create(
358
+ object_type=ContentType.objects.get_for_model(ForwardIngestion),
359
+ object_id=running_ing.pk,
360
+ name="running-ingestion-job",
361
+ user=None,
362
+ status=JobStatusChoices.STATUS_RUNNING,
363
+ job_id="333e4567-e89b-12d3-a456-426614174002",
364
+ created=now,
365
+ started=now,
366
+ completed=None,
367
+ data={},
368
+ )
369
+ running_ing.job = running_job
370
+ running_ing.save(update_fields=["job"])
371
+
372
+ self.client.force_login(self.user)
373
+ with patch("forward_netbox.views.change_explainability_summary") as mock_ce:
374
+ response = self.client.get(
375
+ reverse(
376
+ "plugins:forward_netbox:forwardingestion_logs",
377
+ kwargs={"pk": running_ing.pk},
378
+ ),
379
+ headers={"HX-Request": "true"},
380
+ )
381
+ self.assertEqual(response.status_code, 200)
382
+ mock_ce.assert_not_called()
383
+
384
+ def test_poll_computes_change_explainability_when_done(self):
385
+ # The completed ingestion still computes it (the deferral is only while
386
+ # the job runs), so the operator sees the breakdown once it finishes.
387
+ self.client.force_login(self.user)
388
+ with patch(
389
+ "forward_netbox.views.change_explainability_summary",
390
+ return_value={"available": False},
391
+ ) as mock_ce:
392
+ self.client.get(
393
+ reverse(
394
+ "plugins:forward_netbox:forwardingestion_logs",
395
+ kwargs={"pk": self.ingestion.pk},
396
+ ),
397
+ headers={"HX-Request": "true"},
398
+ )
399
+ mock_ce.assert_called_once()
400
+
341
401
  def test_export_logs_compacts_large_execution_plan_items(self):
342
402
  sync = ForwardSync.objects.create(
343
403
  name="sync-log-export-plan-items",
@@ -0,0 +1,173 @@
1
+ # Read-only diagnostic for "1 created + 1 deleted every sync" idempotency churn.
2
+ #
3
+ # That pattern is an identity mismatch: one object whose natural key as Forward
4
+ # computes it (the slug/name the query emits) does not equal its key as NetBox
5
+ # stored it, so every sync the apply engine creates the Forward version and
6
+ # deletes the NetBox orphan. Single-snapshot dup scans cannot see it (both keys
7
+ # are individually unique); you have to compare the two key spaces.
8
+ #
9
+ # For each simple-dimension model this audits:
10
+ # * Forward computed keys — from the emitted query rows.
11
+ # * NetBox stored keys — from the live objects (lookup_key_from_object).
12
+ # An object/row is matched if ANY of the model's lookup_sets matches (mirroring
13
+ # the apply engine's multi-set lookup), so a slug change that still name-matches
14
+ # is not a false positive. The leftovers are the churn:
15
+ # * would_create — Forward rows matched by no NetBox object.
16
+ # * would_delete — NetBox objects matched by no Forward row.
17
+ # A model with would_create AND would_delete both small (often 1/1) names the
18
+ # churning object and shows the differing key. NEVER writes anything.
19
+ from .apply_engine_bulk import lookup_key_from_object
20
+ from .apply_engine_bulk import lookup_key_from_values
21
+
22
+ # Identity mirrors apply_engine_bulk's simple-model specs. The Forward query for
23
+ # each model emits the lookup fields directly (slug/name/rd/vid/site_slug); we
24
+ # read those off the row rather than recomputing slugify.
25
+ SIMPLE_MODEL_IDENTITY = {
26
+ "dcim.site": {"lookup_sets": (("slug",), ("name",)), "row": ("slug", "name")},
27
+ "dcim.manufacturer": {
28
+ "lookup_sets": (("slug",), ("name",)),
29
+ "row": ("slug", "name"),
30
+ },
31
+ "dcim.devicerole": {
32
+ "lookup_sets": (("slug",), ("name",)),
33
+ "row": ("slug", "name"),
34
+ },
35
+ "dcim.platform": {"lookup_sets": (("slug",), ("name",)), "row": ("slug", "name")},
36
+ "dcim.devicetype": {"lookup_sets": (("slug",),), "row": ("slug",)},
37
+ "ipam.vrf": {"lookup_sets": (("rd",), ("name",)), "row": ("rd", "name")},
38
+ }
39
+ SIMPLE_MODELS = tuple(SIMPLE_MODEL_IDENTITY)
40
+
41
+
42
+ def _model_class(model_string):
43
+ from dcim.models import DeviceRole
44
+ from dcim.models import DeviceType
45
+ from dcim.models import Manufacturer
46
+ from dcim.models import Platform
47
+ from dcim.models import Site
48
+ from ipam.models import VRF
49
+
50
+ return {
51
+ "dcim.site": Site,
52
+ "dcim.manufacturer": Manufacturer,
53
+ "dcim.devicerole": DeviceRole,
54
+ "dcim.platform": Platform,
55
+ "dcim.devicetype": DeviceType,
56
+ "ipam.vrf": VRF,
57
+ }[model_string]
58
+
59
+
60
+ def audit_model_identity(model_string, rows, *, sample_limit=15):
61
+ """Compare Forward ``rows`` against NetBox objects for ``model_string`` and
62
+ report the would-create / would-delete leftovers (the churn candidates)."""
63
+ identity = SIMPLE_MODEL_IDENTITY[model_string]
64
+ lookup_sets = identity["lookup_sets"]
65
+ row_fields = identity["row"]
66
+ model = _model_class(model_string)
67
+
68
+ # Forward key sets (per lookup_set) from emitted row fields.
69
+ forward_keys = {lookup_set: set() for lookup_set in lookup_sets}
70
+ for row in rows:
71
+ values = {field: (row.get(field) or None) for field in row_fields}
72
+ for lookup_set in lookup_sets:
73
+ key = lookup_key_from_values(values, lookup_set, model_string=model_string)
74
+ if key is not None:
75
+ forward_keys[lookup_set].add(key)
76
+
77
+ netbox_keys = {lookup_set: set() for lookup_set in lookup_sets}
78
+ netbox_count = 0
79
+ would_delete = []
80
+ for obj in model.objects.all().iterator():
81
+ netbox_count += 1
82
+ obj_keys = {
83
+ lookup_set: lookup_key_from_object(
84
+ obj, lookup_set, model_string=model_string
85
+ )
86
+ for lookup_set in lookup_sets
87
+ }
88
+ for lookup_set, key in obj_keys.items():
89
+ if key is not None:
90
+ netbox_keys[lookup_set].add(key)
91
+ matched = any(
92
+ key is not None and key in forward_keys[lookup_set]
93
+ for lookup_set, key in obj_keys.items()
94
+ )
95
+ if not matched:
96
+ # Surface the stored key fields so the slug/name Forward and NetBox
97
+ # disagree on is visible next to the would_create label.
98
+ key_parts = ", ".join(
99
+ f"{field}={getattr(obj, field, None)}" for field in lookup_sets[0]
100
+ )
101
+ would_delete.append(f"{obj} ({key_parts})")
102
+
103
+ would_create = []
104
+ for row in rows:
105
+ values = {field: (row.get(field) or None) for field in row_fields}
106
+ matched = False
107
+ for lookup_set in lookup_sets:
108
+ key = lookup_key_from_values(values, lookup_set, model_string=model_string)
109
+ if key is not None and key in netbox_keys[lookup_set]:
110
+ matched = True
111
+ break
112
+ if not matched:
113
+ label = " / ".join(
114
+ str(values.get(field)) for field in row_fields if values.get(field)
115
+ )
116
+ would_create.append(label or "?")
117
+
118
+ return {
119
+ "model": model_string,
120
+ "forward_rows": len(rows),
121
+ "netbox_count": netbox_count,
122
+ "would_create_count": len(would_create),
123
+ "would_delete_count": len(would_delete),
124
+ "would_create_sample": sorted(would_create)[:sample_limit],
125
+ "would_delete_sample": sorted(would_delete)[:sample_limit],
126
+ # The churn signature: both sides non-empty and small => one logical
127
+ # object with two different keys (create the new, delete the orphan).
128
+ "churn_suspect": bool(would_create) and bool(would_delete),
129
+ }
130
+
131
+
132
+ def audit_apply_identity(sync, *, models=None, sample_limit=15, fetch_rows=None):
133
+ """Audit each enabled simple-dimension model for create/delete key mismatches.
134
+
135
+ ``fetch_rows(sync, model_string) -> list[dict]`` is injectable for tests; the
136
+ default fetches via ``ForwardQueryFetcher``. Read-only.
137
+ """
138
+ enabled = set(sync.get_model_strings())
139
+ requested = models or SIMPLE_MODELS
140
+ selected = [m for m in requested if m in enabled and m in SIMPLE_MODEL_IDENTITY]
141
+ fetch_rows = fetch_rows or _default_fetch_rows
142
+
143
+ results = []
144
+ for model_string in selected:
145
+ rows = fetch_rows(sync, model_string)
146
+ results.append(
147
+ audit_model_identity(model_string, rows, sample_limit=sample_limit)
148
+ )
149
+ return {
150
+ "models_audited": selected,
151
+ "results": results,
152
+ "churn_suspect_models": [r["model"] for r in results if r["churn_suspect"]],
153
+ }
154
+
155
+
156
+ def _default_fetch_rows(sync, model_string):
157
+ from .logging import SyncLogging
158
+ from .query_fetch import ForwardQueryFetcher
159
+
160
+ client = sync.source.get_client()
161
+ fetcher = ForwardQueryFetcher(sync, client, SyncLogging())
162
+ context = fetcher.resolve_context()
163
+ fetcher.run_preflight(context, model_strings=[model_string])
164
+ workloads = fetcher.fetch_workloads(
165
+ context,
166
+ model_strings=[model_string],
167
+ validate_rows=False,
168
+ include_diagnostics=False,
169
+ )
170
+ rows = []
171
+ for workload in workloads:
172
+ rows.extend(workload.upsert_rows or [])
173
+ return rows
@@ -1374,19 +1374,27 @@ class ForwardIngestionLogView(LoginRequiredMixin, View):
1374
1374
  data["execution_state"] = _compact_execution_state_payload(
1375
1375
  json_safe_value(get_execution_display_state(ingestion.sync))
1376
1376
  )
1377
- data["change_explainability"] = change_explainability_summary(ingestion)
1377
+ sync_running = ingestion.job and not ingestion.job.completed
1378
+ merge_running = ingestion.merge_job and not ingestion.merge_job.completed
1379
+ job_running = bool(sync_running or merge_running)
1380
+ # Defer the change-explainability recompute while the job is running: it
1381
+ # is only meaningful once staging/merge completes, and recomputing it on
1382
+ # every poll piles DB load onto the web workers during a long settling
1383
+ # merge (a large platform reclassification can run for minutes) — that
1384
+ # contention is what produces the 504 gateway timeouts the customer sees.
1385
+ data["change_explainability"] = (
1386
+ {"available": False, "reason": "deferred_while_running"}
1387
+ if job_running
1388
+ else change_explainability_summary(ingestion)
1389
+ )
1378
1390
  data["export_logs_url"] = reverse(
1379
1391
  "plugins:forward_netbox:forwardingestion_export_logs",
1380
1392
  kwargs={"pk": ingestion.pk},
1381
1393
  )
1382
1394
 
1383
1395
  if request.htmx:
1384
- sync_running = ingestion.job and not ingestion.job.completed
1385
- merge_running = ingestion.merge_job and not ingestion.merge_job.completed
1386
1396
  anything_ever_ran = ingestion.job or ingestion.merge_job
1387
- data["polling_done"] = (
1388
- bool(anything_ever_ran) and not sync_running and not merge_running
1389
- )
1397
+ data["polling_done"] = bool(anything_ever_ran) and not job_running
1390
1398
  return render(request, self.template_name, data)
1391
1399
 
1392
1400
 
@@ -1447,7 +1455,13 @@ class ForwardIngestionView(generic.ObjectView):
1447
1455
  data["execution_state"] = _compact_execution_state_payload(
1448
1456
  json_safe_value(get_execution_display_state(instance.sync))
1449
1457
  )
1450
- data["change_explainability"] = change_explainability_summary(instance)
1458
+ # Defer change-explainability while the job is running (see
1459
+ # ForwardIngestionLogView): avoids recomputing it under merge contention.
1460
+ data["change_explainability"] = (
1461
+ {"available": False, "reason": "deferred_while_running"}
1462
+ if data["job_running"]
1463
+ else change_explainability_summary(instance)
1464
+ )
1451
1465
  data["export_logs_url"] = reverse(
1452
1466
  "plugins:forward_netbox:forwardingestion_export_logs",
1453
1467
  kwargs={"pk": instance.pk},
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "forward-netbox"
3
- version = "2.2.0"
3
+ version = "2.2.2"
4
4
  description = "NetBox plugin to sync Forward data into NetBox via built-in NQE queries"
5
5
  authors = ["Craig Johnson <craigjohnson@forwardnetworks.com>"]
6
6
  license = "MIT"
File without changes