forward-netbox 2.4.2__tar.gz → 2.4.4__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 (292) hide show
  1. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/PKG-INFO +4 -2
  2. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/README.md +3 -1
  3. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/__init__.py +1 -1
  4. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_devices.nqe +3 -6
  5. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +4 -7
  6. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_bulk_merge.py +51 -0
  7. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_endpoints_import.py +131 -1
  8. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_scope_matched_tags.py +4 -2
  9. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/bulk_merge.py +34 -0
  10. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/health.py +12 -6
  11. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/query_binding_resolution.py +1 -1
  12. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/query_fetch_execution.py +98 -12
  13. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/pyproject.toml +1 -1
  14. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/LICENSE +0 -0
  15. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/api/__init__.py +0 -0
  16. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/api/serializers.py +0 -0
  17. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/api/urls.py +0 -0
  18. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/api/views.py +0 -0
  19. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/choices.py +0 -0
  20. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/exceptions.py +0 -0
  21. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/filtersets.py +0 -0
  22. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/forms.py +0 -0
  23. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/jobs.py +0 -0
  24. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/__init__.py +0 -0
  25. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/__init__.py +0 -0
  26. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  27. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_apply_identity_audit.py +0 -0
  28. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  29. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  30. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  31. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
  32. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_metrics.py +0 -0
  33. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  34. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
  35. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  36. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  37. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
  38. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  39. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_stuck_job_alert.py +0 -0
  40. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
  41. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  42. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0001_initial.py +0 -0
  43. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  44. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  45. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  46. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  47. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  48. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  49. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  50. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  51. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  52. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  53. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  54. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  55. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  56. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  57. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  58. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  59. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  60. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  61. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  62. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  63. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  64. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  65. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  66. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  67. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  68. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  69. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  70. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
  71. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0030_forwarddeviceanalysis_cve_ids.py +0 -0
  72. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/0031_encrypt_source_password.py +0 -0
  73. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/migrations/__init__.py +0 -0
  74. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/models.py +0 -0
  75. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/navigation.py +0 -0
  76. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/__init__.py +0 -0
  77. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  78. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  79. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  80. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  81. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  82. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  83. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  84. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  85. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  86. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  87. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  88. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  89. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  90. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  91. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  92. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  93. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  94. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  95. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
  96. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
  97. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  98. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_models.nqe +0 -0
  99. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  100. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_types.nqe +0 -0
  101. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  102. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  103. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  104. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  105. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  106. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  107. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  108. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  109. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_locations.nqe +0 -0
  110. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  111. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_modules.nqe +0 -0
  112. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  113. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  114. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  115. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  116. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_platforms.nqe +0 -0
  117. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  118. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  119. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  120. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  121. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_vlans.nqe +0 -0
  122. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/forward_vrfs.nqe +0 -0
  123. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  124. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/signals.py +0 -0
  125. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
  126. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tables.py +0 -0
  127. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/template_content.py +0 -0
  128. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  129. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
  130. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  131. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  132. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  133. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  134. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  135. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
  136. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  137. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  138. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  139. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
  140. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
  141. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  142. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
  143. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  144. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  145. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  146. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  147. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
  148. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  149. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  150. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  151. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  152. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  153. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  154. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  155. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  156. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templatetags/__init__.py +0 -0
  157. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  158. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/__init__.py +0 -0
  159. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  160. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  161. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/scenarios.py +0 -0
  162. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_api_usage.py +0 -0
  163. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_api_views.py +0 -0
  164. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  165. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_apply_engine.py +0 -0
  166. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_apply_identity_audit.py +0 -0
  167. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_branching.py +0 -0
  168. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_brand_bar.py +0 -0
  169. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  170. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  171. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_crypto.py +0 -0
  172. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_dependency_preview_summary.py +0 -0
  173. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_device_analysis_query.py +0 -0
  174. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  175. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
  176. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_execution_insights_delete_label.py +0 -0
  177. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_forms.py +0 -0
  178. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_forward_api.py +0 -0
  179. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_health.py +0 -0
  180. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  181. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_interface_naming.py +0 -0
  182. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
  183. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  184. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_issue_rendering.py +0 -0
  185. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_job_compat.py +0 -0
  186. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_job_liveness.py +0 -0
  187. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_jobs.py +0 -0
  188. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_log_export.py +0 -0
  189. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_logging.py +0 -0
  190. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_migrations.py +0 -0
  191. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_models.py +0 -0
  192. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_module_readiness.py +0 -0
  193. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
  194. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  195. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_primary_ip.py +0 -0
  196. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
  197. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  198. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_query_binding.py +0 -0
  199. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  200. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_query_registry.py +0 -0
  201. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_query_variants.py +0 -0
  202. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  203. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
  204. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
  205. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_scope_module_ui.py +0 -0
  206. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sensitive_content.py +0 -0
  207. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_stability_hardening.py +0 -0
  208. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sync.py +0 -0
  209. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sync_aci.py +0 -0
  210. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sync_facade.py +0 -0
  211. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  212. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  213. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_sync_state.py +0 -0
  214. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  215. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  216. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/tests/test_vsys_parent.py +0 -0
  217. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/urls.py +0 -0
  218. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/__init__.py +0 -0
  219. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/api_usage.py +0 -0
  220. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/apply_engine.py +0 -0
  221. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
  222. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  223. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/apply_identity_audit.py +0 -0
  224. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/branch_budget.py +0 -0
  225. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/branching.py +0 -0
  226. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/change_explainability.py +0 -0
  227. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/crypto.py +0 -0
  228. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/density_learning.py +0 -0
  229. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/device_analysis.py +0 -0
  230. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/direct_changes.py +0 -0
  231. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/drift_report.py +0 -0
  232. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/execution_ledger.py +0 -0
  233. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/execution_telemetry.py +0 -0
  234. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  235. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  236. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/forward_api.py +0 -0
  237. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/forward_api_impl.py +0 -0
  238. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  239. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/health_checks.py +0 -0
  240. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  241. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/ingestion_issues.py +0 -0
  242. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/ingestion_merge.py +0 -0
  243. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  244. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/interface_naming.py +0 -0
  245. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/job_compat.py +0 -0
  246. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/job_liveness.py +0 -0
  247. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/json_safe.py +0 -0
  248. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/logging.py +0 -0
  249. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/merge.py +0 -0
  250. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/model_contracts.py +0 -0
  251. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/model_validation.py +0 -0
  252. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/module_readiness.py +0 -0
  253. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
  254. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  255. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  256. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/primary_ip.py +0 -0
  257. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/primary_ip_audit.py +0 -0
  258. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/query_binding.py +0 -0
  259. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/query_diagnostics.py +0 -0
  260. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/query_fetch.py +0 -0
  261. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/query_registry.py +0 -0
  262. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/runtime_guidance.py +0 -0
  263. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
  264. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  265. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sensitive_content.py +0 -0
  266. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/single_branch_executor.py +0 -0
  267. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  268. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  269. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync.py +0 -0
  270. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_aci.py +0 -0
  271. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_cable.py +0 -0
  272. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_contracts.py +0 -0
  273. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_core_models.py +0 -0
  274. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_device.py +0 -0
  275. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_events.py +0 -0
  276. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_execution.py +0 -0
  277. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_facade.py +0 -0
  278. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_interface.py +0 -0
  279. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  280. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_ipam.py +0 -0
  281. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_orchestration.py +0 -0
  282. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_primitives.py +0 -0
  283. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_reporting.py +0 -0
  284. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_routing.py +0 -0
  285. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  286. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  287. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  288. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/sync_state.py +0 -0
  289. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/validation.py +0 -0
  290. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/validation_org_query.py +0 -0
  291. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/utilities/vsys_parent.py +0 -0
  292. {forward_netbox-2.4.2 → forward_netbox-2.4.4}/forward_netbox/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forward-netbox
3
- Version: 2.4.2
3
+ Version: 2.4.4
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
@@ -99,7 +99,9 @@ Latest release requires NetBox `4.6.4` and `netbox-branching` `1.1.0+`. Expand f
99
99
 
100
100
  | Plugin Release | NetBox Version | Status |
101
101
  | --- | --- | --- |
102
- | `v2.4.2` | `4.6.4` required; needs netbox-branching `1.1.0+` | Current release; Fix: endpoint import (`sync_endpoints`) and device-tag sync (`sync_device_tags`) now work with the alias-aware and rules-aware query variants (`forward_devices_with_netbox_aliases`, `forward_device_feature_tags_with_rules`), not just the base queries — operators running the variants saw the toggles silently do nothing (validated live: 355 Avocent endpoints import; `Mgmt_*` tags sync). Adds a **Publish Bundled Queries** button on the sync Health page (beside Refresh Query IDs) and two Health warnings: when an opt-in feature is enabled but no enabled map provides it, and when a base query and its opt-in variant are both enabled (they double-apply rows for the same model and churn — enable one). The alias-aware device query now emits the clean role name (e.g. `ROUTER`) to match the base query — expect a one-time role update on alias-mapped devices. |
102
+ | `v2.4.4` | `4.6.4` required; needs netbox-branching `1.1.0+` | Current release; Fix: SNMP-endpoint import now works on **tag-scoped** syncs — the device-tag include scope silently excluded every endpoint, both query-side and in the plugin's local scope filter (whose scoped-device set was built from modeled devices only, so endpoint rows were always dropped; with prune enabled they would even be deleted). Endpoint import now ignores the include scope (exclude tags still apply) and endpoint names join the scoped set (validated live: 355 Avocent endpoints import under a tag-scoped sync). Also fixes the merge-phase `Tag with this Name already exists` issues: a same-named/same-slug tag already on main is now treated as merged instead of failing the branch's tag create. |
103
+ | `v2.4.3` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.4`; Fix: the pinned-query opt-in Health warning (2.4.1) over-claimed failure — it reads "nothing new syncs" and fires on any pinned map with the feature on, even after the query is fixed, because the Health page can't read a pinned query's contents. Reworded to a "Pinned — can't verify locally" heads-up that points at **Export Live Query Drift** to confirm (`source_matches_bundled`), instead of asserting failure. No behavior change. |
104
+ | `v2.4.2` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.3`; Fix: endpoint import (`sync_endpoints`) and device-tag sync (`sync_device_tags`) now work with the alias-aware and rules-aware query variants (`forward_devices_with_netbox_aliases`, `forward_device_feature_tags_with_rules`), not just the base queries — operators running the variants saw the toggles silently do nothing (validated live: 355 Avocent endpoints import; `Mgmt_*` tags sync). Adds a **Publish Bundled Queries** button on the sync Health page (beside Refresh Query IDs) and two Health warnings: when an opt-in feature is enabled but no enabled map provides it, and when a base query and its opt-in variant are both enabled (they double-apply rows for the same model and churn — enable one). The alias-aware device query now emits the clean role name (e.g. `ROUTER`) to match the base query — expect a one-time role update on alias-mapped devices. |
103
105
  | `v2.4.1` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.2`; Fix: opt-in features (SNMP endpoint import, device-tag sync) silently did nothing on sources that run org-managed **pinned** Forward query IDs predating the feature — the sync Health page now raises an actionable warning instead of a silent badge. Remediation: publish the bundled queries to your Forward org folder (Overwrite on), then use Refresh Query IDs, then re-sync. |
104
106
  | `v2.4.0` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.1`; Fix: the "Import SNMP Endpoints as Devices" toggle now renders on the source form (the field shipped in 2.3.2 but was not in any fieldset, so it never showed), letting operators enable endpoint import from the GUI. |
105
107
  | `v2.3.2` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.0`; Feature: optional import of Forward SNMP endpoints (e.g. Avocent console servers) as NetBox devices — off by default (`sync_endpoints`), enabled per source and scoped by the same device tags. |
@@ -66,7 +66,9 @@ Latest release requires NetBox `4.6.4` and `netbox-branching` `1.1.0+`. Expand f
66
66
 
67
67
  | Plugin Release | NetBox Version | Status |
68
68
  | --- | --- | --- |
69
- | `v2.4.2` | `4.6.4` required; needs netbox-branching `1.1.0+` | Current release; Fix: endpoint import (`sync_endpoints`) and device-tag sync (`sync_device_tags`) now work with the alias-aware and rules-aware query variants (`forward_devices_with_netbox_aliases`, `forward_device_feature_tags_with_rules`), not just the base queries — operators running the variants saw the toggles silently do nothing (validated live: 355 Avocent endpoints import; `Mgmt_*` tags sync). Adds a **Publish Bundled Queries** button on the sync Health page (beside Refresh Query IDs) and two Health warnings: when an opt-in feature is enabled but no enabled map provides it, and when a base query and its opt-in variant are both enabled (they double-apply rows for the same model and churn — enable one). The alias-aware device query now emits the clean role name (e.g. `ROUTER`) to match the base query — expect a one-time role update on alias-mapped devices. |
69
+ | `v2.4.4` | `4.6.4` required; needs netbox-branching `1.1.0+` | Current release; Fix: SNMP-endpoint import now works on **tag-scoped** syncs — the device-tag include scope silently excluded every endpoint, both query-side and in the plugin's local scope filter (whose scoped-device set was built from modeled devices only, so endpoint rows were always dropped; with prune enabled they would even be deleted). Endpoint import now ignores the include scope (exclude tags still apply) and endpoint names join the scoped set (validated live: 355 Avocent endpoints import under a tag-scoped sync). Also fixes the merge-phase `Tag with this Name already exists` issues: a same-named/same-slug tag already on main is now treated as merged instead of failing the branch's tag create. |
70
+ | `v2.4.3` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.4`; Fix: the pinned-query opt-in Health warning (2.4.1) over-claimed failure — it reads "nothing new syncs" and fires on any pinned map with the feature on, even after the query is fixed, because the Health page can't read a pinned query's contents. Reworded to a "Pinned — can't verify locally" heads-up that points at **Export Live Query Drift** to confirm (`source_matches_bundled`), instead of asserting failure. No behavior change. |
71
+ | `v2.4.2` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.3`; Fix: endpoint import (`sync_endpoints`) and device-tag sync (`sync_device_tags`) now work with the alias-aware and rules-aware query variants (`forward_devices_with_netbox_aliases`, `forward_device_feature_tags_with_rules`), not just the base queries — operators running the variants saw the toggles silently do nothing (validated live: 355 Avocent endpoints import; `Mgmt_*` tags sync). Adds a **Publish Bundled Queries** button on the sync Health page (beside Refresh Query IDs) and two Health warnings: when an opt-in feature is enabled but no enabled map provides it, and when a base query and its opt-in variant are both enabled (they double-apply rows for the same model and churn — enable one). The alias-aware device query now emits the clean role name (e.g. `ROUTER`) to match the base query — expect a one-time role update on alias-mapped devices. |
70
72
  | `v2.4.1` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.2`; Fix: opt-in features (SNMP endpoint import, device-tag sync) silently did nothing on sources that run org-managed **pinned** Forward query IDs predating the feature — the sync Health page now raises an actionable warning instead of a silent badge. Remediation: publish the bundled queries to your Forward org folder (Overwrite on), then use Refresh Query IDs, then re-sync. |
71
73
  | `v2.4.0` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.1`; Fix: the "Import SNMP Endpoints as Devices" toggle now renders on the source form (the field shipped in 2.3.2 but was not in any fieldset, so it never showed), letting operators enable endpoint import from the GUI. |
72
74
  | `v2.3.2` | `4.6.4` required; needs netbox-branching `1.1.0+` | Superseded by `v2.4.0`; Feature: optional import of Forward SNMP endpoints (e.g. Avocent console servers) as NetBox devices — off by default (`sync_endpoints`), enabled per source and scoped by the same device tags. |
@@ -8,7 +8,7 @@ class NetboxForwardConfig(PluginConfig):
8
8
  name = "forward_netbox"
9
9
  verbose_name = "Forward"
10
10
  description = "Sync Forward data into NetBox using built-in NQE queries."
11
- version = "2.4.2"
11
+ version = "2.4.4"
12
12
  base_url = "forward"
13
13
  min_version = "4.6.4"
14
14
 
@@ -44,15 +44,12 @@ foreach row in (
44
44
  (
45
45
  // SNMP endpoints (opt-in): generic SSH/SNMP devices Forward collects but
46
46
  // does not model. Generic identity from MIB-2; Avocent overlay by enterprise
47
- // OID 10418. Scoped by the same device tags (endpoints carry tagNames).
47
+ // OID 10418. Device-tag INCLUDE scope narrows modeled devices only —
48
+ // endpoints rarely carry device scoping tags, so requiring them silently
49
+ // excluded every endpoint on scoped syncs. Exclude tags still apply.
48
50
  foreach endpoint in network.endpoints
49
51
  where sync_endpoints
50
52
  where !isEmpty(endpoint.snmpOutputs)
51
- where isEmpty(device_tag_include_tags)
52
- || (device_tag_include_match == "all"
53
- && all(foreach tag in device_tag_include_tags select tag in endpoint.tagNames))
54
- || (device_tag_include_match != "all"
55
- && any(foreach tag in device_tag_include_tags select tag in endpoint.tagNames))
56
53
  where isEmpty(device_tag_exclude_tags)
57
54
  || !any(foreach tag in device_tag_exclude_tags select tag in endpoint.tagNames)
58
55
  let sysDescrOpt = max(foreach o in endpoint.snmpOutputs where o.requestedOid == "1.3.6.1.2.1.1.1" select max(foreach e in o.rawOidEntries select e.rawValue))
@@ -86,16 +86,13 @@ foreach row in (
86
86
  (
87
87
  // SNMP endpoints (opt-in): identical branch to forward_devices.nqe. Generic
88
88
  // SSH/SNMP devices Forward collects but does not model. Generic identity from
89
- // MIB-2; Avocent overlay by enterprise OID 10418. Scoped by the same device
90
- // tags (endpoints carry tagNames).
89
+ // MIB-2; Avocent overlay by enterprise OID 10418. Device-tag INCLUDE scope
90
+ // narrows modeled devices only — endpoints rarely carry device scoping tags,
91
+ // so requiring them silently excluded every endpoint on scoped syncs.
92
+ // Exclude tags still apply.
91
93
  foreach endpoint in network.endpoints
92
94
  where sync_endpoints
93
95
  where !isEmpty(endpoint.snmpOutputs)
94
- where isEmpty(device_tag_include_tags)
95
- || (device_tag_include_match == "all"
96
- && all(foreach tag in device_tag_include_tags select tag in endpoint.tagNames))
97
- || (device_tag_include_match != "all"
98
- && any(foreach tag in device_tag_include_tags select tag in endpoint.tagNames))
99
96
  where isEmpty(device_tag_exclude_tags)
100
97
  || !any(foreach tag in device_tag_exclude_tags select tag in endpoint.tagNames)
101
98
  let sysDescrOpt = max(foreach o in endpoint.snmpOutputs where o.requestedOid == "1.3.6.1.2.1.1.1" select max(foreach e in o.rawOidEntries select e.rawValue))
@@ -569,3 +569,54 @@ class DeferredSelfRefFKSplitTest(TestCase):
569
569
  self.assertIn(upd_key, to_process)
570
570
  self.assertEqual(to_process[upd_key].postchange_data["lag"], 99)
571
571
  self.assertIn(cc.key, to_process[upd_key].depends_on)
572
+
573
+
574
+ class BulkMergeTagNameCollisionTest(TransactionTestCase):
575
+ """A branch tag CREATE that collides by NAME with a main-side tag skips.
576
+
577
+ While a merge applies device UPDATEs (ordered before CREATEs),
578
+ netbox_branching sets device tags by name, get_or_creating the tag on main
579
+ with a new pk. The branch's tag CREATE then violated the unique name
580
+ constraint and surfaced as a ValidationError ingestion issue even though the
581
+ desired end state (tag exists) was already reached.
582
+ """
583
+
584
+ def setUp(self):
585
+ from django.contrib.auth import get_user_model
586
+
587
+ self.user = get_user_model().objects.create_user(username="tag-merge-user")
588
+ self.request = RequestFactory().get(reverse("home"))
589
+ self.request.user = self.user
590
+ self.logger = logging.getLogger("forward_netbox.tests.bulk_merge")
591
+
592
+ def test_tag_create_name_collision_skips_instead_of_failing(self):
593
+ from extras.models import Tag
594
+
595
+ branch = provision_branch(user=self.user, name="Tag Collision")
596
+ with activate_branch(branch), event_tracking(self.request):
597
+ self.request.id = uuid.uuid4()
598
+ self.request.user = self.user
599
+ Tag.objects.create(name="Mgmt_Coll", slug="mgmt-coll")
600
+
601
+ # Simulate the mid-merge name-based get_or_create on main: same name,
602
+ # different pk than the branch row.
603
+ Tag.objects.create(name="Mgmt_Coll", slug="mgmt-coll")
604
+
605
+ changes = branch.get_unmerged_changes().order_by("time")
606
+ apply_one = Mock(return_value=False) # fallback would record an issue
607
+ applied, failed, _models = bulk_merge_changes(
608
+ branch,
609
+ changes,
610
+ self.request,
611
+ self.user,
612
+ self.logger,
613
+ apply_one=apply_one,
614
+ )
615
+
616
+ self.assertEqual(failed, 0)
617
+ # The colliding create must be skipped in the batch path, never routed
618
+ # to the per-object fallback (which would raise the ValidationError).
619
+ for call in apply_one.call_args_list:
620
+ collapsed = call.args[0]
621
+ self.assertNotEqual(collapsed.key[0], "extras.tag")
622
+ self.assertEqual(Tag.objects.filter(name="Mgmt_Coll").count(), 1)
@@ -74,9 +74,13 @@ class OptInPinnedDriftElevationTest(SimpleTestCase):
74
74
  self.assertEqual(drift[0]["severity"], "warn")
75
75
  self.assertEqual(drift[0]["status"], "direct_query_id_optin_stale_risk")
76
76
  self.assertIn("Refresh Query IDs", drift[0]["remediation"])
77
+ # Wording is a "can't verify locally" heads-up pointing at the live
78
+ # check, not a confirmed failure.
79
+ self.assertIn("Live Query Drift", drift[0]["remediation"])
80
+ self.assertIn("can't inspect locally", drift[0]["message"])
77
81
  # Badge label must track the elevated status, not the stale build-time one.
78
82
  self.assertNotEqual(drift[0].get("status_label"), "Org-managed (pinned)")
79
- self.assertIn("predate", drift[0].get("status_label", ""))
83
+ self.assertIn("verify", drift[0].get("status_label", ""))
80
84
 
81
85
  def test_endpoints_disabled_leaves_pinned_device_query_silent(self):
82
86
  drift = [self._pinned("forward_devices.nqe")]
@@ -131,3 +135,129 @@ class EndpointFormRenderTest(TestCase):
131
135
  "sync_endpoints field exists but is not in any FieldSet, so the "
132
136
  "form never renders the toggle.",
133
137
  )
138
+
139
+
140
+ from forward_netbox.utilities.query_fetch_execution import ( # noqa: E402
141
+ ForwardQueryFetcher,
142
+ )
143
+
144
+
145
+ class EndpointScopeUnionTest(TestCase):
146
+ """Tag-scoped syncs must keep opt-in endpoint rows, not silently drop them.
147
+
148
+ scoped_device_names was built from network.devices only, so the local scope
149
+ filter removed every endpoint row (and prune would delete them). With
150
+ sync_endpoints on, endpoint names join the scoped set; exclude tags still
151
+ apply to the endpoint probe.
152
+ """
153
+
154
+ def _fetcher(self, client):
155
+ from unittest.mock import Mock
156
+
157
+ return ForwardQueryFetcher(Mock(), client, Mock())
158
+
159
+ def test_scope_union_includes_endpoint_names(self):
160
+ from unittest.mock import Mock
161
+
162
+ client = Mock()
163
+ client.run_nqe_query.side_effect = [
164
+ [{"name": "dev-1", "site": "dc1", "tagNames": ["ACI"]}], # device scope
165
+ [{"name": "avocent-1"}, {"name": "avocent-2"}], # endpoint probe
166
+ ]
167
+ fetcher = self._fetcher(client)
168
+ names, _sites, _matched, failed = fetcher._resolve_scoped_tag_scope(
169
+ network_id="n",
170
+ snapshot_id="s",
171
+ include_tags=["ACI"],
172
+ exclude_tags=[],
173
+ include_match="any",
174
+ sync_endpoints=True,
175
+ )
176
+ self.assertEqual(names, {"dev-1", "avocent-1", "avocent-2"})
177
+ self.assertFalse(failed)
178
+
179
+ def test_scope_without_endpoints_flag_is_unchanged(self):
180
+ from unittest.mock import Mock
181
+
182
+ client = Mock()
183
+ client.run_nqe_query.return_value = [
184
+ {"name": "dev-1", "site": "dc1", "tagNames": ["ACI"]}
185
+ ]
186
+ fetcher = self._fetcher(client)
187
+ names, _sites, _matched, failed = fetcher._resolve_scoped_tag_scope(
188
+ network_id="n",
189
+ snapshot_id="s",
190
+ include_tags=["ACI"],
191
+ exclude_tags=[],
192
+ include_match="any",
193
+ )
194
+ self.assertEqual(names, {"dev-1"})
195
+ self.assertFalse(failed)
196
+ client.run_nqe_query.assert_called_once()
197
+
198
+ def test_endpoint_probe_honors_exclude_tags(self):
199
+ from unittest.mock import Mock
200
+
201
+ client = Mock()
202
+ client.run_nqe_query.return_value = [{"name": "avocent-1"}]
203
+ fetcher = self._fetcher(client)
204
+ fetcher._resolve_scoped_endpoint_names(
205
+ network_id="n", snapshot_id="s", exclude_tags=["Decom"]
206
+ )
207
+ query = client.run_nqe_query.call_args.kwargs["query"]
208
+ self.assertIn("network.endpoints", query)
209
+ self.assertIn('"Decom" in endpoint.tagNames', query)
210
+
211
+ def test_endpoint_probe_failure_warns_and_returns_none(self):
212
+ from unittest.mock import Mock
213
+
214
+ from forward_netbox.exceptions import ForwardClientError
215
+
216
+ client = Mock()
217
+ client.run_nqe_query.side_effect = ForwardClientError("boom")
218
+ fetcher = self._fetcher(client)
219
+ names = fetcher._resolve_scoped_endpoint_names(
220
+ network_id="n", snapshot_id="s", exclude_tags=[]
221
+ )
222
+ self.assertIsNone(names)
223
+ fetcher.logger.log_warning.assert_called_once()
224
+
225
+ def test_endpoint_probe_failure_flags_scope_as_failed(self):
226
+ # If the probe fails but the query still emitted endpoint rows, the
227
+ # local filter would drop them — and prune would DELETE previously
228
+ # imported endpoints. The failure flag lets resolve_context disable
229
+ # endpoint emission for the run instead.
230
+ from unittest.mock import Mock
231
+
232
+ from forward_netbox.exceptions import ForwardClientError
233
+
234
+ client = Mock()
235
+ client.run_nqe_query.side_effect = [
236
+ [{"name": "dev-1", "site": "dc1", "tagNames": ["ACI"]}],
237
+ ForwardClientError("boom"),
238
+ ]
239
+ fetcher = self._fetcher(client)
240
+ names, _sites, _matched, failed = fetcher._resolve_scoped_tag_scope(
241
+ network_id="n",
242
+ snapshot_id="s",
243
+ include_tags=["ACI"],
244
+ exclude_tags=[],
245
+ include_match="any",
246
+ sync_endpoints=True,
247
+ )
248
+ self.assertEqual(names, {"dev-1"})
249
+ self.assertTrue(failed)
250
+
251
+
252
+ class EndpointBranchIncludeScopeRemovalTest(SimpleTestCase):
253
+ """The endpoint branch must not gate on device INCLUDE tags (exclude only)."""
254
+
255
+ def test_endpoint_branches_do_not_filter_include_tags(self):
256
+ for filename in (
257
+ "forward_devices.nqe",
258
+ "forward_devices_with_netbox_aliases.nqe",
259
+ ):
260
+ src = _read_query(filename)
261
+ endpoint_branch = src.split("network.endpoints", 1)[1]
262
+ self.assertIn("device_tag_exclude_tags", endpoint_branch, filename)
263
+ self.assertNotIn("device_tag_include_tags", endpoint_branch, filename)
@@ -41,7 +41,9 @@ class ScopeMatchedTagsResolveTest(TestCase):
41
41
  {"name": "d3", "site": "s", "tagNames": ["TagA", "TagB"]},
42
42
  {"name": "d4", "site": "s", "tagNames": ["TagX"]},
43
43
  ]
44
- names, sites, matched = self._fetcher(client)._resolve_scoped_tag_scope(
44
+ names, sites, matched, _failed = self._fetcher(
45
+ client
46
+ )._resolve_scoped_tag_scope(
45
47
  network_id="net-1",
46
48
  snapshot_id="snap",
47
49
  include_tags=["TagA", "TagB"],
@@ -65,7 +67,7 @@ class ScopeMatchedTagsResolveTest(TestCase):
65
67
  exclude_tags=[],
66
68
  include_match="any",
67
69
  )
68
- self.assertEqual(result, (set(), set(), {}))
70
+ self.assertEqual(result, (set(), set(), {}, False))
69
71
 
70
72
  def test_context_as_dict_carries_full_matched_map(self):
71
73
  ctx = ForwardQueryContext(
@@ -28,6 +28,7 @@ from collections import defaultdict
28
28
  from django.db import DEFAULT_DB_ALIAS
29
29
  from django.db import IntegrityError
30
30
  from django.db import transaction
31
+ from django.db.models import Q
31
32
  from mptt.models import MPTTModel
32
33
  from netbox_branching.merge_strategies.squash import ActionType
33
34
  from netbox_branching.merge_strategies.squash import CollapsedChange
@@ -398,6 +399,31 @@ def bulk_merge_changes(
398
399
  existing = set(
399
400
  model_class.objects.filter(pk__in=all_pks).values_list("pk", flat=True)
400
401
  )
402
+ # Tags collide by NAME, not pk: while the merge applies device UPDATEs
403
+ # (ordered before CREATEs), netbox_branching sets device tags by name,
404
+ # which get_or_creates the tag on main with a NEW pk. The branch's tag
405
+ # CREATE then violates the unique name/slug constraint. Treat a
406
+ # same-named (or same-slug) main-side tag as already merged. If the
407
+ # branch create carried different non-unique attrs (color/description),
408
+ # they converge on the next sync: its branch is provisioned from a main
409
+ # that has the tag, so the apply-time coalesce records an UPDATE.
410
+ existing_names: set[str] = set()
411
+ existing_slugs: set[str] = set()
412
+ if getattr(model_class._meta, "label_lower", "") == "extras.tag":
413
+ pending_names = [
414
+ str((c.postchange_data or {}).get("name") or "") for c in pending
415
+ ]
416
+ pending_slugs = [
417
+ str((c.postchange_data or {}).get("slug") or "") for c in pending
418
+ ]
419
+ name_filter = Q(name__in=[n for n in pending_names if n]) | Q(
420
+ slug__in=[s for s in pending_slugs if s]
421
+ )
422
+ for name, slug in model_class.objects.filter(name_filter).values_list(
423
+ "name", "slug"
424
+ ):
425
+ existing_names.add(str(name))
426
+ existing_slugs.add(str(slug))
401
427
  skipped = 0
402
428
  objects = []
403
429
  built = [] # (collapsed, deserialized) for rows we actually create
@@ -405,6 +431,14 @@ def bulk_merge_changes(
405
431
  if collapsed_change.key[1] in existing:
406
432
  skipped += 1
407
433
  continue
434
+ if existing_names or existing_slugs:
435
+ data = collapsed_change.postchange_data or {}
436
+ if (
437
+ str(data.get("name") or "") in existing_names
438
+ or str(data.get("slug") or "") in existing_slugs
439
+ ):
440
+ skipped += 1
441
+ continue
408
442
  try:
409
443
  deserialized = _deserialize(
410
444
  model_class, collapsed_change, change_logger
@@ -136,17 +136,23 @@ def _elevate_optin_pinned_query_drift(query_drift, source_parameters):
136
136
  # stale "Org-managed (pinned)" badge on the elevated row.
137
137
  item["status_label"] = _QUERY_DRIFT_STATUS_LABELS.get(
138
138
  "direct_query_id_optin_stale_risk",
139
- "Pinned — may predate an enabled feature",
139
+ "Pinned — can't verify locally",
140
140
  )
141
141
  item["severity"] = "warn"
142
+ # This is a "can't verify locally" heads-up, not a confirmed failure:
143
+ # the Health page can't read a pinned query's contents, so it warns for
144
+ # any pinned map with the feature on — including one that's already
145
+ # current after publishing. Point at the live check, don't claim failure.
142
146
  item["message"] = (
143
- f"“{label}” is enabled, but this map runs a pinned Forward query ID. "
144
- "If that query predates the feature it is silently ignored, so "
145
- "nothing new syncs."
147
+ f"“{label}” is enabled and this map runs a pinned Forward query ID, "
148
+ "which the Health page can't inspect locally to confirm it includes "
149
+ "the feature. If those rows aren't importing, the pinned query may "
150
+ "predate the feature."
146
151
  )
147
152
  item["remediation"] = (
148
- "Publish the bundled queries to your Forward org folder (Overwrite "
149
- "on), then use Refresh Query IDs on this Health page, then re-run the "
153
+ "Use Export Live Query Drift on this page to confirm the pinned "
154
+ "query matches the current bundled source. If it doesn't, use "
155
+ "Publish Bundled Queries, then Refresh Query IDs, then re-run the "
150
156
  "sync."
151
157
  )
152
158
  item["remediation_action"] = "refresh_query_ids"
@@ -436,7 +436,7 @@ def _committed_query_source(committed_query: dict) -> str:
436
436
  # read like an error.
437
437
  _QUERY_DRIFT_STATUS_LABELS = {
438
438
  "direct_query_id_unverified": "Org-managed (pinned)",
439
- "direct_query_id_optin_stale_risk": "Pinned — may predate an enabled feature",
439
+ "direct_query_id_optin_stale_risk": "Pinned — can't verify locally",
440
440
  }
441
441
 
442
442
 
@@ -392,6 +392,7 @@ class ForwardQueryFetcher:
392
392
  tuple(exclude_tags),
393
393
  include_match,
394
394
  prune_out_of_scope,
395
+ sync_endpoints,
395
396
  branch_run_state.get("ingestion_id"),
396
397
  branch_run_state.get("pending_ingestion_id"),
397
398
  branch_run_state.get("current_ingestion_id"),
@@ -406,6 +407,7 @@ class ForwardQueryFetcher:
406
407
  include_tags=include_tags,
407
408
  exclude_tags=exclude_tags,
408
409
  include_match=include_match,
410
+ sync_endpoints=sync_endpoints,
409
411
  )
410
412
  cached_context = self._load_context_artifact(context_artifact)
411
413
  if cached_context is None:
@@ -428,21 +430,30 @@ class ForwardQueryFetcher:
428
430
  scoped_device_names,
429
431
  scoped_site_names,
430
432
  scoped_matched_tags,
433
+ endpoint_scope_failed,
431
434
  ) = self._resolve_scoped_tag_scope(
432
435
  network_id=network_id,
433
436
  snapshot_id=snapshot_id,
434
437
  include_tags=include_tags,
435
438
  exclude_tags=exclude_tags,
436
439
  include_match=include_match,
440
+ sync_endpoints=sync_endpoints,
437
441
  )
438
- self._save_context_artifact(
439
- context_artifact,
440
- snapshot_info=snapshot_info,
441
- snapshot_metrics=snapshot_metrics,
442
- scoped_device_names=scoped_device_names,
443
- scoped_site_names=scoped_site_names,
444
- scoped_matched_tags=scoped_matched_tags,
445
- )
442
+ if endpoint_scope_failed:
443
+ # The scoped set carries no endpoint names, so emitting endpoint
444
+ # rows would get them dropped locally — and pruned as deletes.
445
+ # Disable endpoint emission for this run and do NOT persist the
446
+ # endpoint-less scope under the endpoints-on artifact key.
447
+ sync_endpoints = False
448
+ else:
449
+ self._save_context_artifact(
450
+ context_artifact,
451
+ snapshot_info=snapshot_info,
452
+ snapshot_metrics=snapshot_metrics,
453
+ scoped_device_names=scoped_device_names,
454
+ scoped_site_names=scoped_site_names,
455
+ scoped_matched_tags=scoped_matched_tags,
456
+ )
446
457
  else:
447
458
  snapshot_info = dict(cached_context.get("snapshot_info") or {})
448
459
  snapshot_metrics = dict(cached_context.get("snapshot_metrics") or {})
@@ -483,13 +494,16 @@ class ForwardQueryFetcher:
483
494
  include_tags: list[str],
484
495
  exclude_tags: list[str],
485
496
  include_match: str,
497
+ sync_endpoints: bool = False,
486
498
  ) -> dict[str, Any] | None:
487
499
  run = active_execution_run(self.sync)
488
500
  if run is None:
489
501
  return None
490
502
  artifact_run_id = f"shared-sync-{getattr(self.sync, 'pk', 'unknown')}"
491
503
  payload = {
492
- "version": 2,
504
+ # v3: scoped_device_names now varies with the endpoint-import
505
+ # toggle, so the cache key must too.
506
+ "version": 3,
493
507
  "artifact_scope": "query_context",
494
508
  "cache_scope": "shared_sync",
495
509
  "sync_id": getattr(self.sync, "pk", None),
@@ -499,6 +513,7 @@ class ForwardQueryFetcher:
499
513
  "device_tag_include": sorted(include_tags or []),
500
514
  "device_tag_exclude": sorted(exclude_tags or []),
501
515
  "device_tag_include_match": include_match or "any",
516
+ "sync_endpoints": bool(sync_endpoints),
502
517
  }
503
518
  return {
504
519
  "key": fetch_artifact_key(payload),
@@ -575,9 +590,10 @@ class ForwardQueryFetcher:
575
590
  include_tags: list[str],
576
591
  exclude_tags: list[str],
577
592
  include_match: str,
578
- ) -> tuple[set[str], set[str], dict[str, list[str]]]:
593
+ sync_endpoints: bool = False,
594
+ ) -> tuple[set[str], set[str], dict[str, list[str]], bool]:
579
595
  if not include_tags and not exclude_tags:
580
- return set(), set(), {}
596
+ return set(), set(), {}, False
581
597
  scope_where = build_device_tag_scope_where(
582
598
  include_tags, exclude_tags, include_match
583
599
  )
@@ -656,7 +672,77 @@ class ForwardQueryFetcher:
656
672
  exclude_tags=exclude_tags,
657
673
  include_match=include_match,
658
674
  )
659
- return names, sites, matched_tags_by_device
675
+ endpoint_scope_failed = False
676
+ if sync_endpoints:
677
+ endpoint_names = self._resolve_scoped_endpoint_names(
678
+ network_id=network_id,
679
+ snapshot_id=snapshot_id,
680
+ exclude_tags=exclude_tags,
681
+ )
682
+ if endpoint_names is None:
683
+ endpoint_scope_failed = True
684
+ else:
685
+ names |= endpoint_names
686
+ return names, sites, matched_tags_by_device, endpoint_scope_failed
687
+
688
+ def _resolve_scoped_endpoint_names(
689
+ self,
690
+ *,
691
+ network_id: str,
692
+ snapshot_id: str,
693
+ exclude_tags: list[str],
694
+ ) -> set[str] | None:
695
+ """Names of SNMP endpoints that opt-in endpoint import will emit.
696
+
697
+ Device-tag include scope narrows the modeled-device universe; SNMP
698
+ endpoints (e.g. Avocent console servers) rarely carry those device
699
+ scoping tags, so requiring them silently excludes every endpoint. When
700
+ endpoint import is enabled the endpoint names are unioned into the
701
+ scoped-device set so the local scope filter (and out-of-scope prune)
702
+ keeps their rows. Exclude tags still apply as the safety valve.
703
+
704
+ Returns ``None`` when the probe fails. The caller must then disable
705
+ endpoint emission for the run entirely (not just proceed with an empty
706
+ set): the device query would still emit endpoint rows, the local scope
707
+ filter would drop them, and with prune-out-of-scope enabled the dropped
708
+ rows would be emitted as DELETES of previously imported endpoints.
709
+ """
710
+ where = ["where !isEmpty(endpoint.snmpOutputs)"]
711
+ for tag in exclude_tags:
712
+ where.append(f"where !({_nqe_string_literal(tag)} in endpoint.tagNames)")
713
+ query = "\n".join(
714
+ [
715
+ "foreach endpoint in network.endpoints",
716
+ *where,
717
+ "select { name: endpoint.name }",
718
+ ]
719
+ )
720
+ try:
721
+ rows = self.client.run_nqe_query(
722
+ query=query,
723
+ network_id=network_id,
724
+ snapshot_id=snapshot_id,
725
+ fetch_all=True,
726
+ )
727
+ except (ForwardClientError, ForwardConnectivityError, ForwardQueryError) as exc:
728
+ self.logger.log_warning(
729
+ "Forward endpoint scope probe failed; SNMP endpoint import is "
730
+ f"disabled for this run: {_safe_exception_summary(exc)}",
731
+ obj=self.sync,
732
+ )
733
+ return None
734
+ endpoint_names = {
735
+ str(row.get("name") or "").strip()
736
+ for row in rows
737
+ if str(row.get("name") or "").strip()
738
+ }
739
+ if endpoint_names:
740
+ self.logger.log_info(
741
+ f"Added {len(endpoint_names)} SNMP endpoint(s) to the device tag "
742
+ "scope for opt-in endpoint import.",
743
+ obj=self.sync,
744
+ )
745
+ return endpoint_names
660
746
 
661
747
  def _warn_if_scope_all_backfilled(
662
748
  self,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "forward-netbox"
3
- version = "2.4.2"
3
+ version = "2.4.4"
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