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.
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/PKG-INFO +3 -2
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/README.md +2 -1
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/__init__.py +2 -2
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/forms.py +57 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/jobs.py +34 -4
- forward_netbox-2.2.5/forward_netbox/queries/forward_device_feature_tags.nqe +26 -0
- forward_netbox-2.2.5/forward_netbox/tests/test_dependency_preview_summary.py +45 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_forms.py +31 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_jobs.py +28 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_models.py +43 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_query_registry.py +25 -4
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync.py +62 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/model_validation.py +6 -1
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_fetch_execution.py +16 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_registry.py +7 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/views.py +17 -8
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/pyproject.toml +1 -1
- forward_netbox-2.2.4/forward_netbox/queries/forward_device_feature_tags.nqe +0 -20
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/LICENSE +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/serializers.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/urls.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/api/views.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/choices.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/exceptions.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/filtersets.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_apply_identity_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0001_initial.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/0030_forwarddeviceanalysis_cve_ids.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/migrations/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/models.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/navigation.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_models.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_types.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_devices.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_interfaces.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_locations.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_modules.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_platforms.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_vlans.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/forward_vrfs.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/queries/netbox_utilities.nqe +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/signals.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tables.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/template_content.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templatetags/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/scenarios.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_api_usage.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_api_views.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_apply_engine.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_apply_identity_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_branching.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_brand_bar.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_bulk_merge.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_device_analysis_query.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_execution_insights_delete_label.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_forward_api.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_health.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_ingestion_merge.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_interface_naming.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_issue_rendering.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_job_compat.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_job_liveness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_log_export.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_logging.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_module_readiness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_plugin_integrations.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_primary_ip.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_query_binding.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_scope_module_ui.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sensitive_content.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_stability_hardening.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_aci.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_facade.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_orchestration.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_sync_state.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/tests/test_vsys_parent.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/urls.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/api_usage.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_engine.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_engine_decision.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/apply_identity_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/branch_budget.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/branching.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/bulk_merge.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/change_explainability.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/density_learning.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/device_analysis.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/direct_changes.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/drift_report.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/execution_ledger.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/execution_telemetry.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/fetch_artifacts.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/forward_api.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/forward_api_impl.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health_apply_fetch.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health_checks.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/health_summary_blocks.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/ingestion_issues.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/ingestion_merge.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/ingestion_presentation.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/interface_naming.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/job_compat.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/job_liveness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/json_safe.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/logging.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/merge.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/model_contracts.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/module_readiness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/primary_ip.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/primary_ip_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_binding.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_binding_resolution.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_diagnostics.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/query_fetch.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/runtime_guidance.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/scope_reconciliation.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sensitive_content.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/single_branch_executor.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/snapshot_freshness.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/support_bundle_archive.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_aci.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_cable.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_contracts.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_core_models.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_device.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_events.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_execution.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_facade.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_interface.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_inventory_module.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_ipam.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_orchestration.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_primitives.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_reporting.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_routing.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_routing_impl.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/sync_state.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/validation.py +0 -0
- {forward_netbox-2.2.4 → forward_netbox-2.2.5}/forward_netbox/utilities/validation_org_query.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
11
|
+
version = "2.2.5"
|
|
12
12
|
base_url = "forward"
|
|
13
|
-
min_version = "4.6.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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("
|
|
1622
|
-
self.assertIn(
|
|
1623
|
-
self.assertIn(
|
|
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 (
|
|
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(
|