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