forward-netbox 2.2.4__tar.gz → 2.2.5__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 (285) hide show
  1. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/PKG-INFO +3 -2
  2. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/README.md +2 -1
  3. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/__init__.py +2 -2
  4. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/forms.py +57 -0
  5. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/jobs.py +34 -4
  6. forward_netbox-2.2.5/forward_netbox/queries/forward_device_feature_tags.nqe +26 -0
  7. forward_netbox-2.2.5/forward_netbox/tests/test_dependency_preview_summary.py +45 -0
  8. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_forms.py +31 -0
  9. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_jobs.py +28 -0
  10. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_models.py +43 -0
  11. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_query_registry.py +25 -4
  12. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync.py +62 -0
  13. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/model_validation.py +6 -1
  14. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_fetch_execution.py +16 -0
  15. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_registry.py +7 -0
  16. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/views.py +17 -8
  17. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/pyproject.toml +1 -1
  18. forward_netbox-2.2.4/forward_netbox/queries/forward_device_feature_tags.nqe +0 -20
  19. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/LICENSE +0 -0
  20. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/__init__.py +0 -0
  21. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/serializers.py +0 -0
  22. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/urls.py +0 -0
  23. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/views.py +0 -0
  24. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/choices.py +0 -0
  25. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/exceptions.py +0 -0
  26. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/filtersets.py +0 -0
  27. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/__init__.py +0 -0
  28. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/__init__.py +0 -0
  29. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  30. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_apply_identity_audit.py +0 -0
  31. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  32. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  33. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  34. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
  35. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  36. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
  37. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  38. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  39. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
  40. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  41. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
  42. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  43. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0001_initial.py +0 -0
  44. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  45. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  46. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  47. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  48. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  49. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  50. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  51. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  52. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  53. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  54. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  55. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  56. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  57. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  58. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  59. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  60. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  61. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  62. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  63. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  64. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  65. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  66. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  67. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  68. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  69. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  70. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  71. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
  72. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0030_forwarddeviceanalysis_cve_ids.py +0 -0
  73. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/__init__.py +0 -0
  74. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/models.py +0 -0
  75. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/navigation.py +0 -0
  76. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/__init__.py +0 -0
  77. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  78. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  79. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  80. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  81. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  82. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  83. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  84. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  85. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  86. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  87. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  88. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  89. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  90. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  91. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  92. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  93. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  94. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  95. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
  96. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  97. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_models.nqe +0 -0
  98. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  99. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_types.nqe +0 -0
  100. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  101. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_devices.nqe +0 -0
  102. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
  103. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  104. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  105. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  106. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  107. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  108. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  109. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  110. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_locations.nqe +0 -0
  111. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  112. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_modules.nqe +0 -0
  113. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  114. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  115. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  116. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  117. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_platforms.nqe +0 -0
  118. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  119. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  120. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  121. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  122. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_vlans.nqe +0 -0
  123. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_vrfs.nqe +0 -0
  124. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  125. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/signals.py +0 -0
  126. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
  127. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tables.py +0 -0
  128. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/template_content.py +0 -0
  129. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  130. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
  131. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  132. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  133. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  134. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  135. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  136. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
  137. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  138. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  139. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  140. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
  141. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
  142. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  143. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
  144. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  145. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  146. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  147. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  148. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
  149. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  150. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  151. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  152. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  153. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  154. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  155. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  156. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  157. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templatetags/__init__.py +0 -0
  158. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  159. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/__init__.py +0 -0
  160. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  161. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  162. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/scenarios.py +0 -0
  163. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_api_usage.py +0 -0
  164. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_api_views.py +0 -0
  165. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  166. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_apply_engine.py +0 -0
  167. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_apply_identity_audit.py +0 -0
  168. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_branching.py +0 -0
  169. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_brand_bar.py +0 -0
  170. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  171. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_bulk_merge.py +0 -0
  172. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  173. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_device_analysis_query.py +0 -0
  174. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  175. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
  176. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_execution_insights_delete_label.py +0 -0
  177. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_forward_api.py +0 -0
  178. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_health.py +0 -0
  179. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  180. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_interface_naming.py +0 -0
  181. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
  182. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  183. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_issue_rendering.py +0 -0
  184. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_job_compat.py +0 -0
  185. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_job_liveness.py +0 -0
  186. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_log_export.py +0 -0
  187. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_logging.py +0 -0
  188. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_module_readiness.py +0 -0
  189. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
  190. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  191. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_primary_ip.py +0 -0
  192. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
  193. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  194. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_query_binding.py +0 -0
  195. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  196. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  197. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
  198. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
  199. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
  200. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_scope_module_ui.py +0 -0
  201. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sensitive_content.py +0 -0
  202. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_stability_hardening.py +0 -0
  203. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_aci.py +0 -0
  204. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_facade.py +0 -0
  205. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  206. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  207. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_state.py +0 -0
  208. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  209. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  210. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_vsys_parent.py +0 -0
  211. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/urls.py +0 -0
  212. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/__init__.py +0 -0
  213. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/api_usage.py +0 -0
  214. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_engine.py +0 -0
  215. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
  216. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  217. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_identity_audit.py +0 -0
  218. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/branch_budget.py +0 -0
  219. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/branching.py +0 -0
  220. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/bulk_merge.py +0 -0
  221. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/change_explainability.py +0 -0
  222. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/density_learning.py +0 -0
  223. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/device_analysis.py +0 -0
  224. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/direct_changes.py +0 -0
  225. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/drift_report.py +0 -0
  226. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/execution_ledger.py +0 -0
  227. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/execution_telemetry.py +0 -0
  228. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  229. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  230. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/forward_api.py +0 -0
  231. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/forward_api_impl.py +0 -0
  232. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health.py +0 -0
  233. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  234. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health_checks.py +0 -0
  235. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  236. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/ingestion_issues.py +0 -0
  237. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/ingestion_merge.py +0 -0
  238. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  239. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/interface_naming.py +0 -0
  240. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/job_compat.py +0 -0
  241. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/job_liveness.py +0 -0
  242. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/json_safe.py +0 -0
  243. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/logging.py +0 -0
  244. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/merge.py +0 -0
  245. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/model_contracts.py +0 -0
  246. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/module_readiness.py +0 -0
  247. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
  248. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  249. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  250. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/primary_ip.py +0 -0
  251. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/primary_ip_audit.py +0 -0
  252. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_binding.py +0 -0
  253. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_binding_resolution.py +0 -0
  254. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_diagnostics.py +0 -0
  255. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_fetch.py +0 -0
  256. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/runtime_guidance.py +0 -0
  257. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
  258. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  259. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sensitive_content.py +0 -0
  260. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/single_branch_executor.py +0 -0
  261. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  262. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  263. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync.py +0 -0
  264. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_aci.py +0 -0
  265. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_cable.py +0 -0
  266. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_contracts.py +0 -0
  267. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_core_models.py +0 -0
  268. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_device.py +0 -0
  269. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_events.py +0 -0
  270. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_execution.py +0 -0
  271. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_facade.py +0 -0
  272. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_interface.py +0 -0
  273. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  274. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_ipam.py +0 -0
  275. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_orchestration.py +0 -0
  276. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_primitives.py +0 -0
  277. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_reporting.py +0 -0
  278. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_routing.py +0 -0
  279. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  280. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  281. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  282. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_state.py +0 -0
  283. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/validation.py +0 -0
  284. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/validation_org_query.py +0 -0
  285. {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/vsys_parent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forward-netbox
3
- Version: 2.2.4
3
+ Version: 2.2.5
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.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Hotfix: device-analysis NQE (bare foreach) errored refresh + CVE list; surface job errors into job.data |
34
+ | `v2.2.5` | `4.6.4` required; needs netbox-branching `1.1.0+` | Current release; Feature: operator-selectable **Sync Device Tags** — pick which Forward device tags (e.g. `Mgmt_*`) become NetBox device tags (replaces the hardcoded feature-tag set); Fix dependency-preview AttributeError + vsys job pile-up guard (hung pending); test/require NetBox 4.6.4 |
35
+ | `v2.2.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.5`; Hotfix: device-analysis NQE (bare foreach) errored refresh + CVE list; surface job errors into job.data |
35
36
  | `v2.2.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.4`; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
36
37
  | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.3`; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
37
38
  | `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
@@ -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.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Hotfix: device-analysis NQE (bare foreach) errored refresh + CVE list; surface job errors into job.data |
11
+ | `v2.2.5` | `4.6.4` required; needs netbox-branching `1.1.0+` | Current release; Feature: operator-selectable **Sync Device Tags** — pick which Forward device tags (e.g. `Mgmt_*`) become NetBox device tags (replaces the hardcoded feature-tag set); Fix dependency-preview AttributeError + vsys job pile-up guard (hung pending); test/require NetBox 4.6.4 |
12
+ | `v2.2.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.5`; Hotfix: device-analysis NQE (bare foreach) errored refresh + CVE list; surface job errors into job.data |
12
13
  | `v2.2.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.4`; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
13
14
  | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.3`; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
14
15
  | `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
@@ -8,9 +8,9 @@ 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.4"
11
+ version = "2.2.5"
12
12
  base_url = "forward"
13
- min_version = "4.6.3"
13
+ min_version = "4.6.4"
14
14
 
15
15
  def ready(self):
16
16
  super().ready()
@@ -436,6 +436,19 @@ class ForwardSourceForm(NetBoxModelForm):
436
436
  "filtered in NetBox."
437
437
  ),
438
438
  )
439
+ self.fields["sync_device_tags"] = FlexibleMultipleChoiceField(
440
+ required=False,
441
+ choices=(),
442
+ widget=APISelectMultiple(
443
+ api_url="/api/plugins/forward/source/available-tags/"
444
+ ),
445
+ label="Sync Device Tags",
446
+ help_text=(
447
+ "Forward device tags (e.g. Mgmt_*) to sync onto NetBox devices as "
448
+ "device tags. Select one or more; leave empty to sync none. "
449
+ "Independent of the include/exclude scope filters above."
450
+ ),
451
+ )
439
452
  _configure_api_select(
440
453
  self.fields["network_id"].widget,
441
454
  {
@@ -448,6 +461,7 @@ class ForwardSourceForm(NetBoxModelForm):
448
461
  )
449
462
  self.fields["device_tag_include_tags"].widget.attrs["multiple"] = "multiple"
450
463
  self.fields["device_tag_exclude_tags"].widget.attrs["multiple"] = "multiple"
464
+ self.fields["sync_device_tags"].widget.attrs["multiple"] = "multiple"
451
465
  _configure_api_select(
452
466
  self.fields["device_tag_include_tags"].widget,
453
467
  {
@@ -470,6 +484,17 @@ class ForwardSourceForm(NetBoxModelForm):
470
484
  "network_id": "$network_id",
471
485
  },
472
486
  )
487
+ _configure_api_select(
488
+ self.fields["sync_device_tags"].widget,
489
+ {
490
+ "type": "$type",
491
+ "url": "$url",
492
+ "username": "$username",
493
+ "password": "$password",
494
+ "verify": "$verify",
495
+ "network_id": "$network_id",
496
+ },
497
+ )
473
498
  if self.instance.pk:
474
499
  self.fields["network_id"].widget.add_query_param(
475
500
  "source_id", self.instance.pk
@@ -480,6 +505,9 @@ class ForwardSourceForm(NetBoxModelForm):
480
505
  self.fields["device_tag_exclude_tags"].widget.add_query_param(
481
506
  "source_id", self.instance.pk
482
507
  )
508
+ self.fields["sync_device_tags"].widget.add_query_param(
509
+ "source_id", self.instance.pk
510
+ )
483
511
 
484
512
  parameters = self.instance.parameters or {}
485
513
  existing_username = parameters.get("username")
@@ -549,9 +577,11 @@ class ForwardSourceForm(NetBoxModelForm):
549
577
  self.fields["network_id"].choices = _selected_choice(existing_network_id)
550
578
  include_bound = []
551
579
  exclude_bound = []
580
+ sync_tags_bound = []
552
581
  if self.is_bound:
553
582
  include_bound = self._bound_tag_values("device_tag_include_tags")
554
583
  exclude_bound = self._bound_tag_values("device_tag_exclude_tags")
584
+ sync_tags_bound = self._bound_tag_values("sync_device_tags")
555
585
  include_initial = (
556
586
  include_bound
557
587
  if self.is_bound
@@ -568,14 +598,22 @@ class ForwardSourceForm(NetBoxModelForm):
568
598
  if exclude_initial is None and parameters.get("device_tag_exclude"):
569
599
  exclude_initial = [parameters.get("device_tag_exclude")]
570
600
  exclude_initial = self._normalize_tag_values(exclude_initial)
601
+ sync_tags_initial = (
602
+ sync_tags_bound if self.is_bound else parameters.get("sync_device_tags")
603
+ )
604
+ sync_tags_initial = self._normalize_tag_values(sync_tags_initial)
571
605
  self.fields["device_tag_include_tags"].initial = include_initial
572
606
  self.fields["device_tag_exclude_tags"].initial = exclude_initial
607
+ self.fields["sync_device_tags"].initial = sync_tags_initial
573
608
  self.fields["device_tag_include_tags"].choices = [
574
609
  (tag, tag) for tag in include_initial
575
610
  ]
576
611
  self.fields["device_tag_exclude_tags"].choices = [
577
612
  (tag, tag) for tag in exclude_initial
578
613
  ]
614
+ self.fields["sync_device_tags"].choices = [
615
+ (tag, tag) for tag in sync_tags_initial
616
+ ]
579
617
  self.fields["device_tag_include_match"].initial = (
580
618
  parameters.get("device_tag_include_match") or "any"
581
619
  )
@@ -619,6 +657,7 @@ class ForwardSourceForm(NetBoxModelForm):
619
657
  "device_tag_filter_mode",
620
658
  "device_tag_prune_out_of_scope",
621
659
  "apply_device_scope_tags",
660
+ "sync_device_tags",
622
661
  name="Parameters",
623
662
  )
624
663
  )
@@ -649,6 +688,7 @@ class ForwardSourceForm(NetBoxModelForm):
649
688
  "device_tag_filter_mode",
650
689
  "device_tag_prune_out_of_scope",
651
690
  "apply_device_scope_tags",
691
+ "sync_device_tags",
652
692
  name="Parameters",
653
693
  )
654
694
  )
@@ -690,6 +730,11 @@ class ForwardSourceForm(NetBoxModelForm):
690
730
  exclude_tags = self._normalize_tag_values(
691
731
  self._bound_tag_values("device_tag_exclude_tags")
692
732
  )
733
+ sync_device_tags = self._normalize_tag_values(cleaned.get("sync_device_tags"))
734
+ if not sync_device_tags:
735
+ sync_device_tags = self._normalize_tag_values(
736
+ self._bound_tag_values("sync_device_tags")
737
+ )
693
738
  candidate_parameters = {
694
739
  "username": username,
695
740
  "password": password,
@@ -794,6 +839,7 @@ class ForwardSourceForm(NetBoxModelForm):
794
839
  cleaned.get("device_tag_prune_out_of_scope")
795
840
  ),
796
841
  "apply_device_scope_tags": bool(cleaned.get("apply_device_scope_tags")),
842
+ "sync_device_tags": sync_device_tags,
797
843
  }
798
844
  self.instance.type = source_type
799
845
  self.instance.url = (
@@ -834,6 +880,9 @@ class ForwardSourceForm(NetBoxModelForm):
834
880
  self.fields["device_tag_exclude_tags"].choices = [
835
881
  (tag, tag) for tag in exclude_tags
836
882
  ]
883
+ self.fields["sync_device_tags"].choices = [
884
+ (tag, tag) for tag in sync_device_tags
885
+ ]
837
886
  return cleaned
838
887
 
839
888
  def save(self, *args, **kwargs):
@@ -861,6 +910,13 @@ class ForwardSourceForm(NetBoxModelForm):
861
910
  exclude_tags = self._normalize_tag_values(
862
911
  self._bound_tag_values("device_tag_exclude_tags")
863
912
  )
913
+ sync_device_tags = self._normalize_tag_values(
914
+ self.cleaned_data.get("sync_device_tags")
915
+ )
916
+ if not sync_device_tags:
917
+ sync_device_tags = self._normalize_tag_values(
918
+ self._bound_tag_values("sync_device_tags")
919
+ )
864
920
  self.instance.parameters = {
865
921
  "username": self.cleaned_data.get("username")
866
922
  or existing_parameters.get("username")
@@ -976,6 +1032,7 @@ class ForwardSourceForm(NetBoxModelForm):
976
1032
  "apply_device_scope_tags": bool(
977
1033
  self.cleaned_data.get("apply_device_scope_tags")
978
1034
  ),
1035
+ "sync_device_tags": sync_device_tags,
979
1036
  }
980
1037
  self.instance.status = ForwardSourceStatusChoices.NEW
981
1038
  return super().save(*args, **kwargs)
@@ -121,6 +121,24 @@ def record_timeout_issue(ingestion, phase, message):
121
121
  )
122
122
 
123
123
 
124
+ def _sync_has_active_job(sync, name):
125
+ """True if a pending/running job with ``name`` already exists for ``sync``.
126
+
127
+ Post-sync overlays are enqueued after EVERY sync; a slow/large overlay (e.g.
128
+ the vsys parent-link's full-network fetch) can still be running when the next
129
+ sync fires, so without this guard duplicate jobs pile up in PENDING behind it
130
+ and look 'hung'. Skipping the enqueue when one is already active keeps at most
131
+ one overlay of each kind queued per sync.
132
+ """
133
+ return sync.jobs.filter(
134
+ name=name,
135
+ status__in=[
136
+ JobStatusChoices.STATUS_PENDING,
137
+ JobStatusChoices.STATUS_RUNNING,
138
+ ],
139
+ ).exists()
140
+
141
+
124
142
  def _maybe_enqueue_device_analysis_refresh(sync):
125
143
  """Opt-in: after a successful sync, refresh the device-analysis overlay.
126
144
 
@@ -132,11 +150,14 @@ def _maybe_enqueue_device_analysis_refresh(sync):
132
150
  try:
133
151
  from django.utils.module_loading import import_string
134
152
 
153
+ name = f"{sync.name} - refresh device analysis (auto)"
154
+ if _sync_has_active_job(sync, name):
155
+ return
135
156
  Job.enqueue(
136
157
  import_string("forward_netbox.jobs.refresh_forward_device_analysis"),
137
158
  instance=sync,
138
159
  user=sync.user,
139
- name=f"{sync.name} - refresh device analysis (auto)",
160
+ name=name,
140
161
  )
141
162
  except Exception as exc: # pragma: no cover - defensive
142
163
  logger.warning("Auto device-analysis refresh enqueue failed: %s", exc)
@@ -156,11 +177,14 @@ def _maybe_enqueue_backfilled_tag_refresh(sync):
156
177
  try:
157
178
  from django.utils.module_loading import import_string
158
179
 
180
+ name = f"{sync.name} - tag backfilled devices (auto)"
181
+ if _sync_has_active_job(sync, name):
182
+ return
159
183
  Job.enqueue(
160
184
  import_string("forward_netbox.jobs.tag_forward_backfilled_devices"),
161
185
  instance=sync,
162
186
  user=sync.user,
163
- name=f"{sync.name} - tag backfilled devices (auto)",
187
+ name=name,
164
188
  )
165
189
  except Exception as exc: # pragma: no cover - defensive
166
190
  logger.warning("Auto backfilled-tag refresh enqueue failed: %s", exc)
@@ -180,11 +204,14 @@ def _maybe_enqueue_vsys_parent_link(sync):
180
204
  try:
181
205
  from django.utils.module_loading import import_string
182
206
 
207
+ name = f"{sync.name} - link vsys/vdom parents (auto)"
208
+ if _sync_has_active_job(sync, name):
209
+ return
183
210
  Job.enqueue(
184
211
  import_string("forward_netbox.jobs.link_forward_vsys_parents"),
185
212
  instance=sync,
186
213
  user=sync.user,
187
- name=f"{sync.name} - link vsys/vdom parents (auto)",
214
+ name=name,
188
215
  )
189
216
  except Exception as exc: # pragma: no cover - defensive
190
217
  logger.warning("Auto vsys parent-link enqueue failed: %s", exc)
@@ -202,11 +229,14 @@ def _maybe_enqueue_auto_prune(sync):
202
229
  try:
203
230
  from django.utils.module_loading import import_string
204
231
 
232
+ name = f"{sync.name} - prune orphans (auto)"
233
+ if _sync_has_active_job(sync, name):
234
+ return
205
235
  Job.enqueue(
206
236
  import_string("forward_netbox.jobs.prune_forward_orphans"),
207
237
  instance=sync,
208
238
  user=sync.user,
209
- name=f"{sync.name} - prune orphans (auto)",
239
+ name=name,
210
240
  )
211
241
  except Exception as exc: # pragma: no cover - defensive
212
242
  logger.warning("Auto prune-orphans enqueue failed: %s", exc)
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @intent Forward Device Feature Tags
3
+ * @description NetBox device tags synced from Forward. Emits the Forward device
4
+ * tags (device.tagNames) the sync selected in `sync_device_tags`, so operators
5
+ * choose exactly which Forward tags (e.g. Mgmt_*) become NetBox device tags. With
6
+ * no tags selected it emits nothing.
7
+ */
8
+
9
+ @query
10
+ f(forward_netbox_shard_keys: List<String>, sync_device_tags: List<String>) =
11
+ foreach device in network.devices
12
+ where device.snapshotInfo.result == DeviceSnapshotResult.completed
13
+ where device.platform.vendor != Vendor.FORWARD_CUSTOM
14
+ where isEmpty(forward_netbox_shard_keys) || device.name in forward_netbox_shard_keys
15
+ foreach tag in device.tagNames
16
+ where tag in sync_device_tags
17
+ select distinct {
18
+ device: device.name,
19
+ tag: tag,
20
+ tag_slug: replaceRegexMatches(
21
+ replaceRegexMatches(toLowerCase(tag), re`[^a-z0-9]+`, "-"),
22
+ re`^-+|-+$`,
23
+ ""
24
+ ),
25
+ tag_color: "9e9e9e"
26
+ };
@@ -0,0 +1,45 @@
1
+ from django.test import SimpleTestCase
2
+
3
+ from forward_netbox.utilities.query_fetch_execution import ForwardModelResult
4
+ from forward_netbox.views import _dependency_model_result_summary
5
+
6
+
7
+ class DependencyModelResultSummaryTest(SimpleTestCase):
8
+ """Regression: fetcher.model_results are ForwardModelResult dataclasses, but
9
+ _dependency_model_result_summary called result.get(...) — AttributeError that
10
+ errored the whole dependency preview (hidden as null-data until 2.2.4 surfaced
11
+ job errors). The summary must accept a ForwardModelResult.
12
+ """
13
+
14
+ def _result(self, **kw):
15
+ base = dict(
16
+ model_string="ipam.prefix",
17
+ query_name="Forward Prefixes",
18
+ execution_mode="query_path",
19
+ execution_value="",
20
+ sync_mode="branching",
21
+ row_count=12,
22
+ delete_count=3,
23
+ )
24
+ base.update(kw)
25
+ return ForwardModelResult(**base)
26
+
27
+ def test_summary_accepts_dataclass(self):
28
+ summary = _dependency_model_result_summary(self._result())
29
+ self.assertEqual(summary["model"], "ipam.prefix")
30
+ self.assertEqual(summary["row_count"], 12)
31
+ self.assertEqual(summary["delete_count"], 3)
32
+ # estimated_changes is derived (as_dict has no such field).
33
+ self.assertEqual(summary["estimated_changes"], 15)
34
+
35
+ def test_summary_handles_none_runtime(self):
36
+ # runtime_ms defaults to None on the dataclass; must not blow up.
37
+ summary = _dependency_model_result_summary(self._result(runtime_ms=None))
38
+ self.assertEqual(summary["runtime_ms"], 0.0)
39
+
40
+ def test_summary_still_accepts_plain_dict(self):
41
+ summary = _dependency_model_result_summary(
42
+ {"model": "dcim.device", "row_count": 5, "delete_count": 1}
43
+ )
44
+ self.assertEqual(summary["model"], "dcim.device")
45
+ self.assertEqual(summary["estimated_changes"], 6)
@@ -118,6 +118,37 @@ class ForwardSourceFormTest(TestCase):
118
118
  ["tag-a", "tag-b", "tag-c"],
119
119
  )
120
120
 
121
+ def test_sync_device_tags_uses_multiselect_widget(self):
122
+ # The operator picks 1+ Forward tags to sync as NetBox device tags; the
123
+ # widget must keep every selected tag (same regression class as the
124
+ # include/exclude fields).
125
+ form = ForwardSourceForm()
126
+ self.assertTrue(
127
+ form.fields["sync_device_tags"].widget.allow_multiple_selected,
128
+ "sync_device_tags must use a multiple-select widget",
129
+ )
130
+
131
+ @patch("forward_netbox.forms.ForwardSource.validate_connection")
132
+ def test_sync_device_tags_persist_to_parameters(self, _mock_validate):
133
+ data = self._base_form_data()
134
+ data["sync_device_tags"] = ["Mgmt_Vl211", "N.Patel"]
135
+ form = ForwardSourceForm(data=data)
136
+ self.assertTrue(form.is_valid(), form.errors)
137
+ source = form.save()
138
+ # Order-independent: the query sorts on inject, but persistence keeps the
139
+ # normalized submitted set.
140
+ self.assertEqual(
141
+ sorted(source.parameters["sync_device_tags"]),
142
+ ["Mgmt_Vl211", "N.Patel"],
143
+ )
144
+
145
+ @patch("forward_netbox.forms.ForwardSource.validate_connection")
146
+ def test_sync_device_tags_default_empty(self, _mock_validate):
147
+ form = ForwardSourceForm(data=self._base_form_data())
148
+ self.assertTrue(form.is_valid(), form.errors)
149
+ source = form.save()
150
+ self.assertEqual(source.parameters["sync_device_tags"], [])
151
+
121
152
 
122
153
  class ForwardSyncFormTest(TestCase):
123
154
  def setUp(self):
@@ -98,6 +98,34 @@ class ForwardJobsTest(TestCase):
98
98
  enqueue.assert_called_once()
99
99
  self.assertIn("prune orphans", enqueue.call_args.kwargs["name"])
100
100
 
101
+ def test_overlay_enqueue_skips_when_active_job_exists(self):
102
+ # Regression (hung-pending pile-up): the default-on vsys overlay enqueues
103
+ # after every sync; if one is still pending/running it must NOT enqueue a
104
+ # duplicate that stacks up behind it.
105
+ from django.contrib.contenttypes.models import ContentType
106
+
107
+ from forward_netbox.jobs import _maybe_enqueue_vsys_parent_link
108
+
109
+ name = f"{self.sync.name} - link vsys/vdom parents (auto)"
110
+ Job.objects.create(
111
+ object_type=ContentType.objects.get_for_model(ForwardSync),
112
+ object_id=self.sync.pk,
113
+ name=name,
114
+ status=JobStatusChoices.STATUS_PENDING,
115
+ job_id=uuid4(),
116
+ )
117
+ with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
118
+ _maybe_enqueue_vsys_parent_link(self.sync)
119
+ enqueue.assert_not_called()
120
+
121
+ # A completed job of the same name does NOT block a fresh enqueue.
122
+ Job.objects.filter(object_id=self.sync.pk, name=name).update(
123
+ status=JobStatusChoices.STATUS_COMPLETED
124
+ )
125
+ with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
126
+ _maybe_enqueue_vsys_parent_link(self.sync)
127
+ enqueue.assert_called_once()
128
+
101
129
  def test_record_timeout_issue_creates_single_issue_per_ingestion_phase(self):
102
130
  issue_1 = record_timeout_issue(
103
131
  self.ingestion,
@@ -151,6 +151,49 @@ class ForwardSyncModelTest(TestCase):
151
151
  str(ctx.exception),
152
152
  )
153
153
 
154
+ def test_source_accepts_sync_device_tags_list(self):
155
+ source = ForwardSource(
156
+ name="source-sync-device-tags",
157
+ type="saas",
158
+ url="https://fwd.app",
159
+ parameters={
160
+ "username": "user@example.com",
161
+ "password": "secret",
162
+ "verify": True,
163
+ "timeout": 1200,
164
+ "network_id": "test-network",
165
+ "sync_device_tags": ["Mgmt_Vl211", "N.Patel"],
166
+ },
167
+ )
168
+
169
+ source.clean()
170
+
171
+ self.assertEqual(
172
+ source.parameters["sync_device_tags"], ["Mgmt_Vl211", "N.Patel"]
173
+ )
174
+
175
+ def test_source_rejects_non_list_sync_device_tags(self):
176
+ source = ForwardSource(
177
+ name="source-bad-sync-device-tags",
178
+ type="saas",
179
+ url="https://fwd.app",
180
+ parameters={
181
+ "username": "user@example.com",
182
+ "password": "secret",
183
+ "verify": True,
184
+ "timeout": 1200,
185
+ "network_id": "test-network",
186
+ "sync_device_tags": "Mgmt_Vl211",
187
+ },
188
+ )
189
+
190
+ with self.assertRaises(ValidationError) as ctx:
191
+ source.clean()
192
+
193
+ self.assertIn(
194
+ "`sync_device_tags` must be a list of strings.", str(ctx.exception)
195
+ )
196
+
154
197
  def test_source_preserves_api_requests_per_minute(self):
155
198
  source = ForwardSource(
156
199
  name="source-api-rpm",
@@ -883,6 +883,20 @@ class QueryRegistryTest(TestCase):
883
883
  },
884
884
  )
885
885
 
886
+ def test_feature_tags_query_seeds_sync_device_tags_parameter(self):
887
+ # The device-feature-tags query is operator-driven: its signature declares
888
+ # sync_device_tags, so the seeded parameters must include it (empty) or the
889
+ # param-injection guard never fires and the selected tags never reach the
890
+ # fetch.
891
+ rows = {row["name"]: row for row in builtin_nqe_map_rows()}
892
+ self.assertEqual(
893
+ rows["Forward Device Feature Tags"]["parameters"],
894
+ {
895
+ "forward_netbox_shard_keys": [],
896
+ "sync_device_tags": [],
897
+ },
898
+ )
899
+
886
900
  def test_sync_builtin_queries_seed_empty_shard_parameter(self):
887
901
  rows = {row["name"]: row for row in builtin_nqe_map_rows()}
888
902
  excluded_names = {
@@ -1611,16 +1625,23 @@ class QueryRegistryTest(TestCase):
1611
1625
  (("device", "interface", "remote_device", "remote_interface"),),
1612
1626
  )
1613
1627
 
1614
- def test_device_feature_tag_query_emits_bgp_tag(self):
1628
+ def test_device_feature_tag_query_syncs_operator_selected_tags(self):
1629
+ # 2.2.5: the default feature-tags map is param-driven — it emits exactly the
1630
+ # Forward device tags the operator selected in sync_device_tags, not a
1631
+ # hardcoded Prot_BGP. (BGP tagging moved to the routing plugin.)
1615
1632
  spec = next(
1616
1633
  spec
1617
1634
  for spec in BUILTIN_QUERY_SPECS["extras.taggeditem"]
1618
1635
  if spec.query_name == "Forward Device Feature Tags"
1619
1636
  )
1620
1637
 
1621
- self.assertIn("where isPresent(protocol.bgp)", spec.query)
1622
- self.assertIn('tag: "Prot_BGP"', spec.query)
1623
- self.assertIn('tag_slug: "prot-bgp"', spec.query)
1638
+ self.assertIn("sync_device_tags: List<String>", spec.query)
1639
+ self.assertIn("foreach tag in device.tagNames", spec.query)
1640
+ self.assertIn("where tag in sync_device_tags", spec.query)
1641
+ self.assertIn("tag: tag", spec.query)
1642
+ self.assertNotIn('tag: "Prot_BGP"', spec.query)
1643
+ self.assertNotIn("protocol.bgp", spec.query)
1644
+ self.assertIn("sync_device_tags", spec.parameters)
1624
1645
  self.assertEqual(spec.coalesce_fields, (("device", "tag_slug"),))
1625
1646
 
1626
1647
  def test_optional_device_feature_tag_rules_query_uses_data_file(self):
@@ -6734,6 +6734,68 @@ class ForwardSyncRunnerTest(TestCase):
6734
6734
  },
6735
6735
  )
6736
6736
 
6737
+ def test_apply_context_tag_parameters_injects_sync_device_tags(self):
6738
+ # The operator's selected Forward tags (context.sync_device_tags) must be
6739
+ # injected into any query that declares the sync_device_tags parameter, and
6740
+ # sorted so query results are deterministic across runs.
6741
+ client = Mock()
6742
+ logger = Mock()
6743
+ fetcher = ForwardQueryFetcher(
6744
+ sync=self.sync,
6745
+ client=client,
6746
+ logger_=logger,
6747
+ )
6748
+ context = ForwardQueryContext(
6749
+ network_id="test-network",
6750
+ snapshot_selector=LATEST_PROCESSED_SNAPSHOT,
6751
+ snapshot_id="snapshot-before",
6752
+ sync_device_tags=["N.Patel", "Mgmt_Vl211"],
6753
+ )
6754
+ spec = QuerySpec(
6755
+ model_string="extras.taggeditem",
6756
+ query_name="Forward Device Feature Tags",
6757
+ query=(
6758
+ "@query f(forward_netbox_shard_keys: List<String>, "
6759
+ "sync_device_tags: List<String>) = []"
6760
+ ),
6761
+ parameters={"forward_netbox_shard_keys": [], "sync_device_tags": []},
6762
+ )
6763
+
6764
+ resolved = fetcher._apply_context_tag_parameters(
6765
+ spec, {"forward_netbox_shard_keys": []}, context
6766
+ )
6767
+
6768
+ self.assertEqual(resolved["sync_device_tags"], ["Mgmt_Vl211", "N.Patel"])
6769
+
6770
+ def test_apply_context_tag_parameters_skips_sync_device_tags_when_undeclared(self):
6771
+ # A query that does not declare sync_device_tags must not receive it, or the
6772
+ # Forward engine rejects the fetch with an unexpected-parameter error.
6773
+ client = Mock()
6774
+ logger = Mock()
6775
+ fetcher = ForwardQueryFetcher(
6776
+ sync=self.sync,
6777
+ client=client,
6778
+ logger_=logger,
6779
+ )
6780
+ context = ForwardQueryContext(
6781
+ network_id="test-network",
6782
+ snapshot_selector=LATEST_PROCESSED_SNAPSHOT,
6783
+ snapshot_id="snapshot-before",
6784
+ sync_device_tags=["Mgmt_Vl211"],
6785
+ )
6786
+ spec = QuerySpec(
6787
+ model_string="dcim.interface",
6788
+ query_name="Forward Interfaces",
6789
+ query="@query f(forward_netbox_shard_keys: List<String>) = []",
6790
+ parameters={"forward_netbox_shard_keys": []},
6791
+ )
6792
+
6793
+ resolved = fetcher._apply_context_tag_parameters(
6794
+ spec, {"forward_netbox_shard_keys": []}, context
6795
+ )
6796
+
6797
+ self.assertNotIn("sync_device_tags", resolved)
6798
+
6737
6799
  def test_fetch_spec_rows_rejects_legacy_column_filter_scope(self):
6738
6800
  client = Mock()
6739
6801
  logger = Mock()
@@ -59,6 +59,7 @@ def clean_forward_source(source):
59
59
  "device_tag_filter_mode",
60
60
  "device_tag_prune_out_of_scope",
61
61
  "apply_device_scope_tags",
62
+ "sync_device_tags",
62
63
  }
63
64
  )
64
65
  if invalid:
@@ -81,7 +82,11 @@ def clean_forward_source(source):
81
82
  for key in ("device_tag_include", "device_tag_exclude"):
82
83
  if parameters.get(key) is not None and not isinstance(parameters.get(key), str):
83
84
  raise ValidationError(_(f"`{key}` must be a string."))
84
- for key in ("device_tag_include_tags", "device_tag_exclude_tags"):
85
+ for key in (
86
+ "device_tag_include_tags",
87
+ "device_tag_exclude_tags",
88
+ "sync_device_tags",
89
+ ):
85
90
  if parameters.get(key) is None:
86
91
  continue
87
92
  if not isinstance(parameters.get(key), list) or any(