forward-netbox 2.0.0__tar.gz → 2.0.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.0.0 → forward_netbox-2.0.2}/PKG-INFO +4 -2
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/README.md +3 -1
- forward_netbox-2.0.2/forward_netbox/__init__.py +115 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tables.py +7 -7
- forward_netbox-2.0.2/forward_netbox/tests/test_device_scope_tagging.py +167 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_models.py +17 -0
- forward_netbox-2.0.2/forward_netbox/tests/test_runtime_dependency_check.py +59 -0
- forward_netbox-2.0.2/forward_netbox/tests/test_scope_matched_tags.py +80 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/apply_engine_bulk.py +2 -2
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/multi_branch_lifecycle.py +5 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_fetch_execution.py +43 -5
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync.py +3 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_device.py +60 -47
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_interface.py +6 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/pyproject.toml +1 -1
- forward_netbox-2.0.0/forward_netbox/__init__.py +0 -83
- forward_netbox-2.0.0/forward_netbox/tests/test_device_scope_tagging.py +0 -121
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/LICENSE +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/serializers.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/urls.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/api/views.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/choices.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/exceptions.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/filtersets.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/forms.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/jobs.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0001_initial.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/migrations/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/models.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/navigation.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_analysis.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_models.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_types.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_devices.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_interfaces.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_locations.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_modules.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_platforms.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_vlans.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/forward_vrfs.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/queries/netbox_utilities.nqe +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/signals.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/template_content.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_health.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templatetags/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/scenarios.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_api_usage.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_api_views.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_apply_engine.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_branching.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_bulk_merge.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_forms.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_forward_api.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_health.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_ingestion_merge.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_interface_naming.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_issue_rendering.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_job_compat.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_job_liveness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_jobs.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_log_export.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_logging.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_module_readiness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_plugin_integrations.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_primary_ip.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_query_binding.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_query_registry.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_scope_module_ui.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sensitive_content.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_stability_hardening.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_aci.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_facade.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_orchestration.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_sync_state.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/urls.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/api_usage.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/apply_engine.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/apply_engine_decision.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/branch_budget.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/branching.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/bulk_merge.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/change_explainability.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/density_learning.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/device_analysis.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/direct_changes.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/drift_report.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/execution_ledger.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/execution_telemetry.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/fetch_artifacts.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/forward_api.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/forward_api_impl.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health_apply_fetch.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health_checks.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/health_summary_blocks.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/ingestion_issues.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/ingestion_merge.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/ingestion_presentation.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/interface_naming.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/job_compat.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/job_liveness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/json_safe.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/logging.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/merge.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/model_contracts.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/model_validation.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/module_readiness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/primary_ip.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_binding.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_binding_resolution.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_diagnostics.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_fetch.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/query_registry.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/runtime_guidance.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/scope_reconciliation.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sensitive_content.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/single_branch_executor.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/snapshot_freshness.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/support_bundle_archive.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_aci.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_cable.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_contracts.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_core_models.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_events.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_execution.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_facade.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_inventory_module.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_ipam.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_orchestration.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_primitives.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_reporting.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_routing.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_routing_impl.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/sync_state.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/validation.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/validation_org_query.py +0 -0
- {forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forward-netbox
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.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.0.
|
|
34
|
+
| `v2.0.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Patch: apply_device_scope_tags now works with multiple include tags in `any` match mode — each device is tagged with exactly the include tag(s) it carries (resolved per-device at fetch time), instead of skipping. Also silences the spurious `Skipping untagged VLAN 1` warning (VID 1 is NetBox's implicit access default and is intentionally not imported). No engine/schema changes; drop-in from `2.0.1`, no org republish |
|
|
35
|
+
| `v2.0.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.2`; Patch: fixes two 2.0.0 regressions an operator hits immediately — a false `netbox_branching is not installed; syncs will fail` startup warning (the dependency check used the wrong distribution name), and a 500 on the Sync list page (`KeyError: 'available'` from a removed execution-ledger summary). No engine or data changes; drop-in upgrade from `2.0.0` |
|
|
36
|
+
| `v2.0.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.1`; Breaking 2.0 — single-branch is the only execution path. Removed the per-shard branching/fast-bootstrap/resumable executor, 10k-change budget sharding, and the execution-ledger run-history; dropped the backend/max-changes/scheduler-overlap selectors |
|
|
35
37
|
| `v1.7.2` | `4.6.3` required (4.5.x dropped); needs netbox-branching `1.1.0+` | Superseded by `v2.0.0`; Collection-gap diagnostics: per-reason backfill breakdown + staleness, growth/trend escalation, per-device collection result, ACI delete safety valve, opt-in auto-tag |
|
|
36
38
|
| `v1.7.1` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.2`; ACI BD/L3Out graduation + FHRP churn fix (replaces yanked 1.7.0 and 1.6.2) |
|
|
37
39
|
| `v1.7.0` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.1`; ACI bridge domain and L3Out NQE maps; query publish hardening |
|
|
@@ -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.0.
|
|
11
|
+
| `v2.0.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Patch: apply_device_scope_tags now works with multiple include tags in `any` match mode — each device is tagged with exactly the include tag(s) it carries (resolved per-device at fetch time), instead of skipping. Also silences the spurious `Skipping untagged VLAN 1` warning (VID 1 is NetBox's implicit access default and is intentionally not imported). No engine/schema changes; drop-in from `2.0.1`, no org republish |
|
|
12
|
+
| `v2.0.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.2`; Patch: fixes two 2.0.0 regressions an operator hits immediately — a false `netbox_branching is not installed; syncs will fail` startup warning (the dependency check used the wrong distribution name), and a 500 on the Sync list page (`KeyError: 'available'` from a removed execution-ledger summary). No engine or data changes; drop-in upgrade from `2.0.0` |
|
|
13
|
+
| `v2.0.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.0.1`; Breaking 2.0 — single-branch is the only execution path. Removed the per-shard branching/fast-bootstrap/resumable executor, 10k-change budget sharding, and the execution-ledger run-history; dropped the backend/max-changes/scheduler-overlap selectors |
|
|
12
14
|
| `v1.7.2` | `4.6.3` required (4.5.x dropped); needs netbox-branching `1.1.0+` | Superseded by `v2.0.0`; Collection-gap diagnostics: per-reason backfill breakdown + staleness, growth/trend escalation, per-device collection result, ACI delete safety valve, opt-in auto-tag |
|
|
13
15
|
| `v1.7.1` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.2`; ACI BD/L3Out graduation + FHRP churn fix (replaces yanked 1.7.0 and 1.6.2) |
|
|
14
16
|
| `v1.7.0` | `4.5.9` and `4.6.2` validated; shared branch for `4.5.x` and `4.6.x` with capability-gated 4.6 features | Superseded by `v1.7.1`; ACI bridge domain and L3Out NQE maps; query publish hardening |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from netbox.plugins import PluginConfig
|
|
3
|
+
except ModuleNotFoundError: # pragma: no cover - tooling imports outside NetBox
|
|
4
|
+
PluginConfig = object
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NetboxForwardConfig(PluginConfig):
|
|
8
|
+
name = "forward_netbox"
|
|
9
|
+
verbose_name = "NetBox Forward Plugin"
|
|
10
|
+
description = "Sync Forward data into NetBox using built-in NQE queries."
|
|
11
|
+
version = "2.0.2"
|
|
12
|
+
base_url = "forward"
|
|
13
|
+
min_version = "4.6.3"
|
|
14
|
+
|
|
15
|
+
def ready(self):
|
|
16
|
+
super().ready()
|
|
17
|
+
from . import signals # noqa: F401
|
|
18
|
+
|
|
19
|
+
_check_runtime_dependencies()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _check_runtime_dependencies():
|
|
23
|
+
"""Warn at startup about a missing/old ``netbox_branching`` dependency.
|
|
24
|
+
|
|
25
|
+
The single-branch ingest path hard-depends on netbox-branching 1.1.0
|
|
26
|
+
internals (SquashMergeStrategy collapse/cycle-split/FK-graph, the
|
|
27
|
+
squash_dependency_graph_built signal, branch.connection_name, the
|
|
28
|
+
ObjectChange read router). A missing or pre-1.1.0 install otherwise only
|
|
29
|
+
surfaces mid-sync as an opaque failure. This logs (never raises) so NetBox
|
|
30
|
+
startup is unaffected.
|
|
31
|
+
|
|
32
|
+
"Installed and enabled" is tested by IMPORTABILITY, not the distribution
|
|
33
|
+
name: NetBox only loads the module when it is listed in PLUGINS, and the
|
|
34
|
+
PyPI distribution is ``netboxlabs-netbox-branching`` (not the import name
|
|
35
|
+
``netbox_branching``), so keying the check off a single dist name produced a
|
|
36
|
+
false "not installed" warning on every boot even when it was active.
|
|
37
|
+
"""
|
|
38
|
+
import logging
|
|
39
|
+
|
|
40
|
+
log = logging.getLogger("forward_netbox")
|
|
41
|
+
floor = (1, 1, 0)
|
|
42
|
+
label = "netbox_branching"
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import netbox_branching # noqa: F401
|
|
46
|
+
except Exception:
|
|
47
|
+
log.warning(
|
|
48
|
+
"forward_netbox requires the %s plugin for branch-staged syncs, but "
|
|
49
|
+
"it is not installed/enabled; syncs will fail. Install "
|
|
50
|
+
"netboxlabs-netbox-branching (>=1.1.0), add 'netbox_branching' to "
|
|
51
|
+
"PLUGINS in configuration.py, and run migrations.",
|
|
52
|
+
label,
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
resolved = _resolved_branching_version()
|
|
57
|
+
if resolved is None:
|
|
58
|
+
# Installed/importable but the version could not be resolved (unusual
|
|
59
|
+
# packaging). Do not cry wolf — the import succeeded, so syncs can run.
|
|
60
|
+
log.info("forward_netbox runtime dependency %s installed", label)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
log.info("forward_netbox runtime dependency %s==%s", label, resolved)
|
|
64
|
+
if _version_tuple(resolved) < floor:
|
|
65
|
+
log.warning(
|
|
66
|
+
"forward_netbox requires %s>=%s but found %s; syncs will fail (the "
|
|
67
|
+
"plugin reuses netbox-branching 1.1.0 internals). Upgrade "
|
|
68
|
+
"netboxlabs-netbox-branching.",
|
|
69
|
+
label,
|
|
70
|
+
".".join(str(part) for part in floor),
|
|
71
|
+
resolved,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolved_branching_version():
|
|
76
|
+
"""Best-effort netbox-branching version, robust to the distribution name.
|
|
77
|
+
|
|
78
|
+
The PyPI distribution is ``netboxlabs-netbox-branching``; older/forks may use
|
|
79
|
+
``netbox-branching``. Fall back to the module's ``__version__``.
|
|
80
|
+
"""
|
|
81
|
+
from importlib.metadata import PackageNotFoundError
|
|
82
|
+
from importlib.metadata import version
|
|
83
|
+
|
|
84
|
+
for dist in ("netboxlabs-netbox-branching", "netbox-branching"):
|
|
85
|
+
try:
|
|
86
|
+
return version(dist)
|
|
87
|
+
except PackageNotFoundError:
|
|
88
|
+
continue
|
|
89
|
+
except Exception: # pragma: no cover - never block startup
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
import netbox_branching
|
|
93
|
+
|
|
94
|
+
return getattr(netbox_branching, "__version__", None)
|
|
95
|
+
except Exception: # pragma: no cover - never block startup
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _version_tuple(value):
|
|
100
|
+
"""Best-effort (major, minor, patch) tuple from a version string."""
|
|
101
|
+
parts = []
|
|
102
|
+
for chunk in str(value).split(".")[:3]:
|
|
103
|
+
digits = ""
|
|
104
|
+
for char in chunk:
|
|
105
|
+
if char.isdigit():
|
|
106
|
+
digits += char
|
|
107
|
+
else:
|
|
108
|
+
break
|
|
109
|
+
parts.append(int(digits) if digits else 0)
|
|
110
|
+
while len(parts) < 3:
|
|
111
|
+
parts.append(0)
|
|
112
|
+
return tuple(parts)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
config = NetboxForwardConfig
|
|
@@ -16,8 +16,6 @@ from .models import ForwardNQEMap
|
|
|
16
16
|
from .models import ForwardSource
|
|
17
17
|
from .models import ForwardSync
|
|
18
18
|
from .models import ForwardValidationRun
|
|
19
|
-
from .utilities.execution_ledger import execution_run_failure_summary
|
|
20
|
-
from .utilities.execution_ledger import latest_execution_run
|
|
21
19
|
from .utilities.json_safe import json_safe_value
|
|
22
20
|
|
|
23
21
|
|
|
@@ -90,13 +88,15 @@ class ForwardSyncTable(NetBoxTable):
|
|
|
90
88
|
return getattr(value, "name", "---") if value else "---"
|
|
91
89
|
|
|
92
90
|
def render_latest_failure(self, value, record):
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
# 2.0 removed the execution-run ledger; surface the latest ingestion's
|
|
92
|
+
# failed-change count (the always-populated 2.0 failure signal) instead.
|
|
93
|
+
ingestion = record.forwardingestion_set.last()
|
|
94
|
+
failed = getattr(ingestion, "failed_change_count", 0) or 0
|
|
95
|
+
if not failed:
|
|
95
96
|
return "---"
|
|
96
97
|
return format_html(
|
|
97
|
-
'<span
|
|
98
|
-
|
|
99
|
-
summary["message"],
|
|
98
|
+
'<span class="text-danger">{}</span>',
|
|
99
|
+
_("{n} failed change(s)").format(n=failed),
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
class Meta(NetBoxTable.Meta):
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
from dcim.models import Device
|
|
4
|
+
from django.test import TestCase
|
|
5
|
+
from extras.models import Tag
|
|
6
|
+
|
|
7
|
+
from forward_netbox.models import ForwardSource
|
|
8
|
+
from forward_netbox.models import ForwardSync
|
|
9
|
+
from forward_netbox.utilities.sync import ForwardSyncRunner
|
|
10
|
+
from forward_netbox.utilities.sync_device import apply_dcim_device
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DeviceScopeTaggingTest(TestCase):
|
|
14
|
+
"""apply_device_scope_tags tags each device with exactly the include tags it
|
|
15
|
+
carries. The per-device matched-tag map is resolved at fetch time and set on
|
|
16
|
+
the runner (runner._scope_matched_tags) by the executor."""
|
|
17
|
+
|
|
18
|
+
def _source(
|
|
19
|
+
self, *, apply_scope_tags, include_tags=("N.Patel",), include_match="any"
|
|
20
|
+
):
|
|
21
|
+
return ForwardSource.objects.create(
|
|
22
|
+
name=f"scope-src-{ForwardSource.objects.count()}",
|
|
23
|
+
type="saas",
|
|
24
|
+
url="https://fwd.app",
|
|
25
|
+
status="ready",
|
|
26
|
+
parameters={
|
|
27
|
+
"username": "u@example.com",
|
|
28
|
+
"password": "p",
|
|
29
|
+
"verify": True,
|
|
30
|
+
"network_id": "net-1",
|
|
31
|
+
"device_tag_include_tags": list(include_tags),
|
|
32
|
+
"device_tag_include_match": include_match,
|
|
33
|
+
"apply_device_scope_tags": apply_scope_tags,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _sync(self, source):
|
|
38
|
+
return ForwardSync.objects.create(
|
|
39
|
+
name=f"scope-sync-{source.pk}",
|
|
40
|
+
source=source,
|
|
41
|
+
parameters={"snapshot_id": "latestProcessed"},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _runner(self, sync, matched=None):
|
|
45
|
+
runner = ForwardSyncRunner(
|
|
46
|
+
sync=sync, ingestion=None, client=None, logger_=Mock()
|
|
47
|
+
)
|
|
48
|
+
runner._scope_matched_tags = dict(matched or {})
|
|
49
|
+
return runner
|
|
50
|
+
|
|
51
|
+
def _row(self, name="dev-1"):
|
|
52
|
+
return {
|
|
53
|
+
"name": name,
|
|
54
|
+
"site": "site-a",
|
|
55
|
+
"site_slug": "site-a",
|
|
56
|
+
"role": "role-a",
|
|
57
|
+
"role_slug": "role-a",
|
|
58
|
+
"role_color": "9e9e9e",
|
|
59
|
+
"manufacturer": "Cisco",
|
|
60
|
+
"manufacturer_slug": "cisco",
|
|
61
|
+
"device_type": "model-a",
|
|
62
|
+
"device_type_slug": "model-a",
|
|
63
|
+
"platform": "",
|
|
64
|
+
"platform_slug": "",
|
|
65
|
+
"status": "active",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def test_scope_tag_applied_when_enabled(self):
|
|
69
|
+
sync = self._sync(self._source(apply_scope_tags=True))
|
|
70
|
+
runner = self._runner(sync, matched={"dev-1": ["N.Patel"]})
|
|
71
|
+
apply_dcim_device(runner, self._row())
|
|
72
|
+
|
|
73
|
+
device = Device.objects.get(name="dev-1")
|
|
74
|
+
self.assertEqual(
|
|
75
|
+
sorted(device.tags.values_list("name", flat=True)), ["N.Patel"]
|
|
76
|
+
)
|
|
77
|
+
self.assertEqual(Tag.objects.filter(name="N.Patel").count(), 1)
|
|
78
|
+
|
|
79
|
+
def test_scope_tag_not_applied_when_disabled(self):
|
|
80
|
+
sync = self._sync(self._source(apply_scope_tags=False))
|
|
81
|
+
runner = self._runner(sync, matched={"dev-1": ["N.Patel"]})
|
|
82
|
+
apply_dcim_device(runner, self._row())
|
|
83
|
+
|
|
84
|
+
device = Device.objects.get(name="dev-1")
|
|
85
|
+
self.assertEqual(list(device.tags.all()), [])
|
|
86
|
+
self.assertFalse(Tag.objects.filter(name="N.Patel").exists())
|
|
87
|
+
|
|
88
|
+
def test_reapply_does_not_duplicate_tag(self):
|
|
89
|
+
sync = self._sync(self._source(apply_scope_tags=True))
|
|
90
|
+
apply_dcim_device(self._runner(sync, {"dev-1": ["N.Patel"]}), self._row())
|
|
91
|
+
# Fresh runner (cleared caches) re-applies the same device -> idempotent.
|
|
92
|
+
apply_dcim_device(self._runner(sync, {"dev-1": ["N.Patel"]}), self._row())
|
|
93
|
+
|
|
94
|
+
device = Device.objects.get(name="dev-1")
|
|
95
|
+
self.assertEqual(device.tags.filter(name="N.Patel").count(), 1)
|
|
96
|
+
|
|
97
|
+
def test_multi_tag_any_applies_only_carried_tags(self):
|
|
98
|
+
# The previously-skipped case: multiple include tags, "any" mode. The
|
|
99
|
+
# device carries only TagA, so it gets only TagA — never TagB.
|
|
100
|
+
sync = self._sync(
|
|
101
|
+
self._source(
|
|
102
|
+
apply_scope_tags=True,
|
|
103
|
+
include_tags=["TagA", "TagB"],
|
|
104
|
+
include_match="any",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
runner = self._runner(sync, matched={"dev-1": ["TagA"]})
|
|
108
|
+
apply_dcim_device(runner, self._row())
|
|
109
|
+
|
|
110
|
+
device = Device.objects.get(name="dev-1")
|
|
111
|
+
self.assertEqual(sorted(device.tags.values_list("name", flat=True)), ["TagA"])
|
|
112
|
+
self.assertFalse(device.tags.filter(name="TagB").exists())
|
|
113
|
+
|
|
114
|
+
def test_multi_tag_all_mode_applies_all(self):
|
|
115
|
+
# "all" mode: every in-scope device carries every include tag (the
|
|
116
|
+
# resolver yields the full set), so all are applied — identical to before.
|
|
117
|
+
sync = self._sync(
|
|
118
|
+
self._source(
|
|
119
|
+
apply_scope_tags=True,
|
|
120
|
+
include_tags=["TagA", "TagB"],
|
|
121
|
+
include_match="all",
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
runner = self._runner(sync, matched={"dev-1": ["TagA", "TagB"]})
|
|
125
|
+
apply_dcim_device(runner, self._row())
|
|
126
|
+
|
|
127
|
+
device = Device.objects.get(name="dev-1")
|
|
128
|
+
self.assertEqual(
|
|
129
|
+
sorted(device.tags.values_list("name", flat=True)), ["TagA", "TagB"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def test_stale_scope_tag_removed_user_tag_preserved(self):
|
|
133
|
+
# A device that drops a Forward include tag between syncs loses the
|
|
134
|
+
# corresponding scope tag, but unrelated (user/feature) tags are kept.
|
|
135
|
+
sync = self._sync(
|
|
136
|
+
self._source(
|
|
137
|
+
apply_scope_tags=True,
|
|
138
|
+
include_tags=["TagA", "TagB"],
|
|
139
|
+
include_match="any",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
apply_dcim_device(self._runner(sync, {"dev-1": ["TagA", "TagB"]}), self._row())
|
|
143
|
+
device = Device.objects.get(name="dev-1")
|
|
144
|
+
user_tag, _ = Tag.objects.get_or_create(name="owner-x", slug="owner-x")
|
|
145
|
+
device.tags.add(user_tag)
|
|
146
|
+
self.assertEqual(
|
|
147
|
+
sorted(device.tags.values_list("name", flat=True)),
|
|
148
|
+
["TagA", "TagB", "owner-x"],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Re-sync: device now carries only TagA.
|
|
152
|
+
apply_dcim_device(self._runner(sync, {"dev-1": ["TagA"]}), self._row())
|
|
153
|
+
device.refresh_from_db()
|
|
154
|
+
self.assertEqual(
|
|
155
|
+
sorted(device.tags.values_list("name", flat=True)),
|
|
156
|
+
["TagA", "owner-x"],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def test_unmatched_device_gets_no_scope_tags(self):
|
|
160
|
+
# A device absent from the matched map (e.g. exclude-only scope) is left
|
|
161
|
+
# untagged and does not crash.
|
|
162
|
+
sync = self._sync(self._source(apply_scope_tags=True))
|
|
163
|
+
runner = self._runner(sync, matched={})
|
|
164
|
+
apply_dcim_device(runner, self._row())
|
|
165
|
+
|
|
166
|
+
device = Device.objects.get(name="dev-1")
|
|
167
|
+
self.assertEqual(list(device.tags.all()), [])
|
|
@@ -1591,6 +1591,23 @@ class ForwardSyncModelTest(TestCase):
|
|
|
1591
1591
|
def test_sync_table_shows_scheduled_by_default(self):
|
|
1592
1592
|
self.assertIn("scheduled", ForwardSyncTable.Meta.default_columns)
|
|
1593
1593
|
|
|
1594
|
+
def test_sync_table_latest_failure_renders_without_ledger(self):
|
|
1595
|
+
# Regression: render_latest_failure called the removed execution-ledger
|
|
1596
|
+
# stubs and indexed summary["available"] -> KeyError, 500'ing the sync
|
|
1597
|
+
# list page in 2.0. It must render from the ingestion's failed-change
|
|
1598
|
+
# count and never crash.
|
|
1599
|
+
from forward_netbox.models import ForwardIngestion
|
|
1600
|
+
|
|
1601
|
+
sync = ForwardSync.objects.create(name="lf-sync", source=self.source)
|
|
1602
|
+
table = ForwardSyncTable(ForwardSync.objects.filter(pk=sync.pk))
|
|
1603
|
+
# No ingestion -> graceful dash.
|
|
1604
|
+
self.assertEqual(table.render_latest_failure(value=None, record=sync), "---")
|
|
1605
|
+
# Failed changes -> surfaced count, still no crash.
|
|
1606
|
+
ForwardIngestion.objects.create(sync=sync, failed_change_count=3)
|
|
1607
|
+
rendered = str(table.render_latest_failure(value=None, record=sync))
|
|
1608
|
+
self.assertIn("3", rendered)
|
|
1609
|
+
self.assertNotIn("available", rendered)
|
|
1610
|
+
|
|
1594
1611
|
@patch("forward_netbox.models.ForwardSource.get_client")
|
|
1595
1612
|
@patch(
|
|
1596
1613
|
"forward_netbox.utilities.single_branch_executor.ForwardSingleBranchExecutor"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from django.test import SimpleTestCase
|
|
5
|
+
|
|
6
|
+
from forward_netbox import _check_runtime_dependencies
|
|
7
|
+
from forward_netbox import _resolved_branching_version
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuntimeDependencyCheckTest(SimpleTestCase):
|
|
11
|
+
"""Guards the netbox_branching startup dependency check.
|
|
12
|
+
|
|
13
|
+
Regression: the check keyed off the distribution name ``netbox-branching``,
|
|
14
|
+
but the PyPI distribution is ``netboxlabs-netbox-branching`` — so
|
|
15
|
+
importlib.metadata.version() always raised PackageNotFoundError and the
|
|
16
|
+
plugin logged a false "netbox_branching is not installed; syncs will fail"
|
|
17
|
+
warning on EVERY boot, even when branching was installed and active.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def _capture(self):
|
|
21
|
+
logger = logging.getLogger("forward_netbox")
|
|
22
|
+
with self.assertLogs(logger, level="INFO") as cm:
|
|
23
|
+
_check_runtime_dependencies()
|
|
24
|
+
return "\n".join(cm.output)
|
|
25
|
+
|
|
26
|
+
def test_resolves_version_by_correct_distribution_name(self):
|
|
27
|
+
# netbox-branching is installed as the netboxlabs-netbox-branching dist.
|
|
28
|
+
self.assertIsNotNone(_resolved_branching_version())
|
|
29
|
+
|
|
30
|
+
def test_no_false_not_installed_warning_when_installed(self):
|
|
31
|
+
out = self._capture()
|
|
32
|
+
self.assertNotIn("not installed", out)
|
|
33
|
+
self.assertNotIn("syncs will fail", out)
|
|
34
|
+
|
|
35
|
+
def test_warns_when_below_floor(self):
|
|
36
|
+
with patch("forward_netbox._resolved_branching_version", return_value="1.0.4"):
|
|
37
|
+
out = self._capture()
|
|
38
|
+
self.assertIn("netbox_branching>=1.1.0", out)
|
|
39
|
+
self.assertIn("syncs will fail", out)
|
|
40
|
+
|
|
41
|
+
def test_warns_when_not_importable(self):
|
|
42
|
+
real_import = (
|
|
43
|
+
__builtins__["__import__"]
|
|
44
|
+
if isinstance(__builtins__, dict)
|
|
45
|
+
else __builtins__.__import__
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def fake_import(name, *args, **kwargs):
|
|
49
|
+
if name == "netbox_branching":
|
|
50
|
+
raise ImportError("simulated missing plugin")
|
|
51
|
+
return real_import(name, *args, **kwargs)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger("forward_netbox")
|
|
54
|
+
with patch("builtins.__import__", side_effect=fake_import):
|
|
55
|
+
with self.assertLogs(logger, level="WARNING") as cm:
|
|
56
|
+
_check_runtime_dependencies()
|
|
57
|
+
out = "\n".join(cm.output)
|
|
58
|
+
self.assertIn("not installed/enabled", out)
|
|
59
|
+
self.assertIn("PLUGINS", out)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
|
|
5
|
+
from forward_netbox.models import ForwardSource
|
|
6
|
+
from forward_netbox.models import ForwardSync
|
|
7
|
+
from forward_netbox.utilities.query_fetch import ForwardQueryContext
|
|
8
|
+
from forward_netbox.utilities.query_fetch_execution import ForwardQueryFetcher
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ScopeMatchedTagsResolveTest(TestCase):
|
|
12
|
+
"""The inline scope resolver returns, per in-scope device, exactly the
|
|
13
|
+
include tags that device carries (the apply_device_scope_tags per-device map),
|
|
14
|
+
intersected at resolve time so the persisted payload stays small."""
|
|
15
|
+
|
|
16
|
+
def _fetcher(self, client):
|
|
17
|
+
source = ForwardSource.objects.create(
|
|
18
|
+
name=f"smt-src-{ForwardSource.objects.count()}",
|
|
19
|
+
type="saas",
|
|
20
|
+
url="https://fwd.app",
|
|
21
|
+
status="ready",
|
|
22
|
+
parameters={
|
|
23
|
+
"username": "u",
|
|
24
|
+
"password": "p",
|
|
25
|
+
"verify": True,
|
|
26
|
+
"network_id": "net-1",
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
sync = ForwardSync.objects.create(
|
|
30
|
+
name=f"smt-sync-{source.pk}",
|
|
31
|
+
source=source,
|
|
32
|
+
parameters={"snapshot_id": "latestProcessed"},
|
|
33
|
+
)
|
|
34
|
+
return ForwardQueryFetcher(sync=sync, client=client, logger_=Mock())
|
|
35
|
+
|
|
36
|
+
def test_resolve_returns_per_device_intersection(self):
|
|
37
|
+
client = Mock()
|
|
38
|
+
client.run_nqe_query.return_value = [
|
|
39
|
+
{"name": "d1", "site": "s", "tagNames": ["TagA", "TagX"]},
|
|
40
|
+
{"name": "d2", "site": "s", "tagNames": ["TagB"]},
|
|
41
|
+
{"name": "d3", "site": "s", "tagNames": ["TagA", "TagB"]},
|
|
42
|
+
{"name": "d4", "site": "s", "tagNames": ["TagX"]},
|
|
43
|
+
]
|
|
44
|
+
names, sites, matched = self._fetcher(client)._resolve_scoped_tag_scope(
|
|
45
|
+
network_id="net-1",
|
|
46
|
+
snapshot_id="snap",
|
|
47
|
+
include_tags=["TagA", "TagB"],
|
|
48
|
+
exclude_tags=[],
|
|
49
|
+
include_match="any",
|
|
50
|
+
)
|
|
51
|
+
self.assertEqual(
|
|
52
|
+
matched, {"d1": ["TagA"], "d2": ["TagB"], "d3": ["TagA", "TagB"]}
|
|
53
|
+
)
|
|
54
|
+
# d4 carries none of the include tags -> not in the map (no tagging).
|
|
55
|
+
self.assertNotIn("d4", matched)
|
|
56
|
+
# The query must request tagNames.
|
|
57
|
+
query = client.run_nqe_query.call_args.kwargs["query"]
|
|
58
|
+
self.assertIn("tagNames: device.tagNames", query)
|
|
59
|
+
|
|
60
|
+
def test_resolve_no_tags_returns_empty_map(self):
|
|
61
|
+
result = self._fetcher(Mock())._resolve_scoped_tag_scope(
|
|
62
|
+
network_id="net-1",
|
|
63
|
+
snapshot_id="snap",
|
|
64
|
+
include_tags=[],
|
|
65
|
+
exclude_tags=[],
|
|
66
|
+
include_match="any",
|
|
67
|
+
)
|
|
68
|
+
self.assertEqual(result, (set(), set(), {}))
|
|
69
|
+
|
|
70
|
+
def test_context_as_dict_carries_full_matched_map(self):
|
|
71
|
+
ctx = ForwardQueryContext(
|
|
72
|
+
network_id="net-1",
|
|
73
|
+
snapshot_selector="latestProcessed",
|
|
74
|
+
snapshot_id="snap",
|
|
75
|
+
scoped_matched_tags={"d1": ["TagA"], "d2": ["TagB"]},
|
|
76
|
+
)
|
|
77
|
+
# The branch apply path reads this back — it must be the full map, not a count.
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
ctx.as_dict()["scoped_matched_tags"], {"d1": ["TagA"], "d2": ["TagB"]}
|
|
80
|
+
)
|
|
@@ -1105,7 +1105,7 @@ def bulk_orm_apply_device(runner, rows: list[dict[str, Any]]):
|
|
|
1105
1105
|
|
|
1106
1106
|
from ..exceptions import ForwardDependencySkipError
|
|
1107
1107
|
from ..exceptions import ForwardSearchError
|
|
1108
|
-
from .sync_device import
|
|
1108
|
+
from .sync_device import _scope_tags_enabled
|
|
1109
1109
|
from .sync_device import apply_dcim_device
|
|
1110
1110
|
|
|
1111
1111
|
update_field_names = ["site", "role", "device_type", "platform", "serial", "status"]
|
|
@@ -1138,7 +1138,7 @@ def bulk_orm_apply_device(runner, rows: list[dict[str, Any]]):
|
|
|
1138
1138
|
|
|
1139
1139
|
# Opt-in per-device scope tagging is an adapter side effect; keep exact parity
|
|
1140
1140
|
# by delegating the whole batch when it is enabled.
|
|
1141
|
-
if
|
|
1141
|
+
if _scope_tags_enabled(runner):
|
|
1142
1142
|
for row in rows:
|
|
1143
1143
|
_delegate(row)
|
|
1144
1144
|
runner.events_clearer.clear()
|
{forward_netbox-2.0.0 → forward_netbox-2.0.2}/forward_netbox/utilities/multi_branch_lifecycle.py
RENAMED
|
@@ -85,6 +85,11 @@ def run_item_in_branch(executor, item, context, ingestion, branch, *, total_plan
|
|
|
85
85
|
logger_=executor.logger,
|
|
86
86
|
)
|
|
87
87
|
runner._model_coalesce_fields[item.model_string] = item.coalesce_fields
|
|
88
|
+
# Per-device scope-tag map resolved at fetch time (apply_device_scope_tags);
|
|
89
|
+
# without this, branched syncs would tag nothing.
|
|
90
|
+
runner._scope_matched_tags = {
|
|
91
|
+
str(k): list(v) for k, v in (context.get("scoped_matched_tags") or {}).items()
|
|
92
|
+
}
|
|
88
93
|
ingestion.snapshot_selector = context["snapshot_selector"]
|
|
89
94
|
ingestion.snapshot_id = context["snapshot_id"]
|
|
90
95
|
ingestion.snapshot_info = context["snapshot_info"]
|