forward-netbox 2.2.0__tar.gz → 2.2.1__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.1}/PKG-INFO +3 -2
  2. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/README.md +2 -1
  3. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/__init__.py +1 -1
  4. forward_netbox-2.2.1/forward_netbox/management/commands/forward_apply_identity_audit.py +80 -0
  5. forward_netbox-2.2.1/forward_netbox/tests/test_apply_identity_audit.py +50 -0
  6. forward_netbox-2.2.1/forward_netbox/utilities/apply_identity_audit.py +175 -0
  7. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/pyproject.toml +1 -1
  8. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/LICENSE +0 -0
  9. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/api/__init__.py +0 -0
  10. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/api/serializers.py +0 -0
  11. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/api/urls.py +0 -0
  12. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/api/views.py +0 -0
  13. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/choices.py +0 -0
  14. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/exceptions.py +0 -0
  15. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/filtersets.py +0 -0
  16. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/forms.py +0 -0
  17. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/jobs.py +0 -0
  18. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/__init__.py +0 -0
  19. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/__init__.py +0 -0
  20. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  21. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  22. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  23. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  24. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
  25. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  26. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
  27. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  28. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  29. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
  30. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  31. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
  32. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  33. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0001_initial.py +0 -0
  34. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  35. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  36. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  37. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  38. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  39. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  40. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  41. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  42. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  43. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  44. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  45. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  46. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  47. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  48. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  49. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  50. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  51. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  52. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  53. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  54. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  55. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  56. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  57. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  58. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  59. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  60. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  61. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
  62. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/migrations/__init__.py +0 -0
  63. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/models.py +0 -0
  64. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/navigation.py +0 -0
  65. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/__init__.py +0 -0
  66. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  67. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  68. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  69. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  70. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  71. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  72. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  73. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  74. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  75. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  76. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  77. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  78. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  79. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  80. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  81. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  82. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  83. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  84. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
  85. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
  86. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  87. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_models.nqe +0 -0
  88. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  89. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_types.nqe +0 -0
  90. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  91. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_devices.nqe +0 -0
  92. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
  93. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  94. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  95. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  96. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  97. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  98. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  99. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  100. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_locations.nqe +0 -0
  101. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  102. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_modules.nqe +0 -0
  103. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  104. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  105. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  106. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  107. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_platforms.nqe +0 -0
  108. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  109. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  110. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  111. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  112. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_vlans.nqe +0 -0
  113. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/forward_vrfs.nqe +0 -0
  114. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  115. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/signals.py +0 -0
  116. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
  117. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tables.py +0 -0
  118. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/template_content.py +0 -0
  119. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  120. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
  121. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  122. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  123. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  124. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  125. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  126. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
  127. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  128. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  129. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  130. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
  131. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
  132. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  133. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
  134. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  135. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  136. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  137. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  138. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
  139. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  140. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  141. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  142. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  143. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  144. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  145. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  146. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  147. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templatetags/__init__.py +0 -0
  148. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  149. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/__init__.py +0 -0
  150. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  151. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  152. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/scenarios.py +0 -0
  153. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_api_usage.py +0 -0
  154. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_api_views.py +0 -0
  155. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  156. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_apply_engine.py +0 -0
  157. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_branching.py +0 -0
  158. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_brand_bar.py +0 -0
  159. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  160. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_bulk_merge.py +0 -0
  161. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  162. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  163. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
  164. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_forms.py +0 -0
  165. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_forward_api.py +0 -0
  166. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_health.py +0 -0
  167. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  168. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_interface_naming.py +0 -0
  169. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
  170. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  171. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_issue_rendering.py +0 -0
  172. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_job_compat.py +0 -0
  173. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_job_liveness.py +0 -0
  174. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_jobs.py +0 -0
  175. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_log_export.py +0 -0
  176. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_logging.py +0 -0
  177. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_models.py +0 -0
  178. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_module_readiness.py +0 -0
  179. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
  180. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  181. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_primary_ip.py +0 -0
  182. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
  183. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  184. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_query_binding.py +0 -0
  185. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  186. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_query_registry.py +0 -0
  187. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  188. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
  189. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
  190. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
  191. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_scope_module_ui.py +0 -0
  192. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sensitive_content.py +0 -0
  193. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_stability_hardening.py +0 -0
  194. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sync.py +0 -0
  195. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sync_aci.py +0 -0
  196. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sync_facade.py +0 -0
  197. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  198. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  199. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_sync_state.py +0 -0
  200. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  201. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  202. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/tests/test_vsys_parent.py +0 -0
  203. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/urls.py +0 -0
  204. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/__init__.py +0 -0
  205. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/api_usage.py +0 -0
  206. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/apply_engine.py +0 -0
  207. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
  208. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  209. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/branch_budget.py +0 -0
  210. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/branching.py +0 -0
  211. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/bulk_merge.py +0 -0
  212. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/change_explainability.py +0 -0
  213. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/density_learning.py +0 -0
  214. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/device_analysis.py +0 -0
  215. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/direct_changes.py +0 -0
  216. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/drift_report.py +0 -0
  217. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/execution_ledger.py +0 -0
  218. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/execution_telemetry.py +0 -0
  219. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  220. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  221. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/forward_api.py +0 -0
  222. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/forward_api_impl.py +0 -0
  223. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/health.py +0 -0
  224. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  225. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/health_checks.py +0 -0
  226. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  227. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/ingestion_issues.py +0 -0
  228. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/ingestion_merge.py +0 -0
  229. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  230. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/interface_naming.py +0 -0
  231. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/job_compat.py +0 -0
  232. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/job_liveness.py +0 -0
  233. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/json_safe.py +0 -0
  234. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/logging.py +0 -0
  235. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/merge.py +0 -0
  236. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/model_contracts.py +0 -0
  237. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/model_validation.py +0 -0
  238. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/module_readiness.py +0 -0
  239. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
  240. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  241. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  242. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/primary_ip.py +0 -0
  243. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/primary_ip_audit.py +0 -0
  244. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/query_binding.py +0 -0
  245. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/query_binding_resolution.py +0 -0
  246. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/query_diagnostics.py +0 -0
  247. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/query_fetch.py +0 -0
  248. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/query_fetch_execution.py +0 -0
  249. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/query_registry.py +0 -0
  250. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/runtime_guidance.py +0 -0
  251. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
  252. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  253. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sensitive_content.py +0 -0
  254. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/single_branch_executor.py +0 -0
  255. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  256. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  257. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync.py +0 -0
  258. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_aci.py +0 -0
  259. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_cable.py +0 -0
  260. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_contracts.py +0 -0
  261. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_core_models.py +0 -0
  262. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_device.py +0 -0
  263. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_events.py +0 -0
  264. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_execution.py +0 -0
  265. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_facade.py +0 -0
  266. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_interface.py +0 -0
  267. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  268. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_ipam.py +0 -0
  269. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_orchestration.py +0 -0
  270. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_primitives.py +0 -0
  271. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_reporting.py +0 -0
  272. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_routing.py +0 -0
  273. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  274. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  275. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  276. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/sync_state.py +0 -0
  277. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/validation.py +0 -0
  278. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/validation_org_query.py +0 -0
  279. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/utilities/vsys_parent.py +0 -0
  280. {forward_netbox-2.2.0 → forward_netbox-2.2.1}/forward_netbox/views.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.1
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,8 @@ 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.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
35
+ | `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
36
  | `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
37
  | `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
38
  | `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,8 @@ 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.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
12
+ | `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
13
  | `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
14
  | `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
15
  | `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.1"
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()
@@ -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"], [])
@@ -0,0 +1,175 @@
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
+ forward_row_by_key = {}
71
+ for row in rows:
72
+ values = {field: (row.get(field) or None) for field in row_fields}
73
+ for lookup_set in lookup_sets:
74
+ key = lookup_key_from_values(values, lookup_set, model_string=model_string)
75
+ if key is not None:
76
+ forward_keys[lookup_set].add(key)
77
+ forward_row_by_key.setdefault((lookup_set, key), row)
78
+
79
+ netbox_keys = {lookup_set: set() for lookup_set in lookup_sets}
80
+ netbox_count = 0
81
+ would_delete = []
82
+ for obj in model.objects.all().iterator():
83
+ netbox_count += 1
84
+ obj_keys = {
85
+ lookup_set: lookup_key_from_object(
86
+ obj, lookup_set, model_string=model_string
87
+ )
88
+ for lookup_set in lookup_sets
89
+ }
90
+ for lookup_set, key in obj_keys.items():
91
+ if key is not None:
92
+ netbox_keys[lookup_set].add(key)
93
+ matched = any(
94
+ key is not None and key in forward_keys[lookup_set]
95
+ for lookup_set, key in obj_keys.items()
96
+ )
97
+ if not matched:
98
+ # Surface the stored key fields so the slug/name Forward and NetBox
99
+ # disagree on is visible next to the would_create label.
100
+ key_parts = ", ".join(
101
+ f"{field}={getattr(obj, field, None)}" for field in lookup_sets[0]
102
+ )
103
+ would_delete.append(f"{obj} ({key_parts})")
104
+
105
+ would_create = []
106
+ for row in rows:
107
+ values = {field: (row.get(field) or None) for field in row_fields}
108
+ matched = False
109
+ for lookup_set in lookup_sets:
110
+ key = lookup_key_from_values(values, lookup_set, model_string=model_string)
111
+ if key is not None and key in netbox_keys[lookup_set]:
112
+ matched = True
113
+ break
114
+ if not matched:
115
+ label = " / ".join(
116
+ str(values.get(field)) for field in row_fields if values.get(field)
117
+ )
118
+ would_create.append(label or "?")
119
+
120
+ return {
121
+ "model": model_string,
122
+ "forward_rows": len(rows),
123
+ "netbox_count": netbox_count,
124
+ "would_create_count": len(would_create),
125
+ "would_delete_count": len(would_delete),
126
+ "would_create_sample": sorted(would_create)[:sample_limit],
127
+ "would_delete_sample": sorted(would_delete)[:sample_limit],
128
+ # The churn signature: both sides non-empty and small => one logical
129
+ # object with two different keys (create the new, delete the orphan).
130
+ "churn_suspect": bool(would_create) and bool(would_delete),
131
+ }
132
+
133
+
134
+ def audit_apply_identity(sync, *, models=None, sample_limit=15, fetch_rows=None):
135
+ """Audit each enabled simple-dimension model for create/delete key mismatches.
136
+
137
+ ``fetch_rows(sync, model_string) -> list[dict]`` is injectable for tests; the
138
+ default fetches via ``ForwardQueryFetcher``. Read-only.
139
+ """
140
+ enabled = set(sync.get_model_strings())
141
+ requested = models or SIMPLE_MODELS
142
+ selected = [m for m in requested if m in enabled and m in SIMPLE_MODEL_IDENTITY]
143
+ fetch_rows = fetch_rows or _default_fetch_rows
144
+
145
+ results = []
146
+ for model_string in selected:
147
+ rows = fetch_rows(sync, model_string)
148
+ results.append(
149
+ audit_model_identity(model_string, rows, sample_limit=sample_limit)
150
+ )
151
+ return {
152
+ "models_audited": selected,
153
+ "results": results,
154
+ "churn_suspect_models": [r["model"] for r in results if r["churn_suspect"]],
155
+ }
156
+
157
+
158
+ def _default_fetch_rows(sync, model_string):
159
+ from .logging import SyncLogging
160
+ from .query_fetch import ForwardQueryFetcher
161
+
162
+ client = sync.source.get_client()
163
+ fetcher = ForwardQueryFetcher(sync, client, SyncLogging())
164
+ context = fetcher.resolve_context()
165
+ fetcher.run_preflight(context, model_strings=[model_string])
166
+ workloads = fetcher.fetch_workloads(
167
+ context,
168
+ model_strings=[model_string],
169
+ validate_rows=False,
170
+ include_diagnostics=False,
171
+ )
172
+ rows = []
173
+ for workload in workloads:
174
+ rows.extend(workload.upsert_rows or [])
175
+ return rows
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "forward-netbox"
3
- version = "2.2.0"
3
+ version = "2.2.1"
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