netbox-data-import 2.2.0__tar.gz → 2.4.0__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.
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/PKG-INFO +1 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/__init__.py +1 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/adapters.py +53 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/serializers.py +60 -12
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/urls.py +4 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/views.py +107 -50
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/cable_target.py +39 -43
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/catalog.py +1 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/forms.py +24 -3
- netbox_data_import-2.4.0/netbox_data_import/graphql/schema.py +30 -0
- netbox_data_import-2.4.0/netbox_data_import/graphql/types.py +42 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/import_engine.py +3 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_adapter.py +147 -48
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_backend.py +18 -0
- netbox_data_import-2.4.0/netbox_data_import/inference_connection_test.py +146 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_credentials.py +71 -20
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_settings.py +3 -4
- netbox_data_import-2.4.0/netbox_data_import/inference_transport.py +349 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/ip_assignment.py +72 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/jobs.py +0 -23
- netbox_data_import-2.4.0/netbox_data_import/migrations/0034_tracedeviceresolution.py +41 -0
- netbox_data_import-2.4.0/netbox_data_import/migrations/0035_retire_superseded_proposals.py +28 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/models.py +71 -11
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/netbox_reader.py +6 -0
- netbox_data_import-2.4.0/netbox_data_import/object_permissions.py +677 -0
- netbox_data_import-2.4.0/netbox_data_import/profile_yaml.py +458 -0
- netbox_data_import-2.4.0/netbox_data_import/proposal_contract.py +30 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_jobs.py +12 -17
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_response.py +36 -16
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/review_workspace.py +74 -12
- netbox_data_import-2.4.0/netbox_data_import/source_trace.py +124 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +2 -1
- netbox_data_import-2.4.0/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +130 -0
- netbox_data_import-2.4.0/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +54 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +16 -37
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/target_modules.py +244 -78
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +43 -3
- netbox_data_import-2.4.0/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +45 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +20 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +118 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/termination_proposal.py +14 -6
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/helpers.py +54 -7
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/inference_http.py +7 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/mock_discipline.py +2 -1
- netbox_data_import-2.4.0/netbox_data_import/tests/redaction.py +54 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_atomic_exits.py +7 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_cable_module.py +68 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_configuration_docs.py +55 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_configuration_surfaces.py +1250 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_conflict_row_actions.py +241 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_sync.py +2 -43
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +10 -60
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_device_module.py +190 -15
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_digest_indexed_models.py +18 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_cutover.py +95 -23
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_engine.py +109 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_execution.py +105 -8
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_adapter.py +431 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_inference_connection_test.py +666 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_credentials.py +80 -50
- netbox_data_import-2.4.0/netbox_data_import/tests/test_inference_secret_containment.py +338 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_settings.py +6 -3
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_migrations.py +1 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_mock_discipline.py +11 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_module_boundaries.py +55 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_object_permission_writer.py +609 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_policy_write_refusals.py +155 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_pristine_preview_rows.py +2 -18
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_jobs.py +82 -4
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_response.py +36 -16
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_tasks.py +12 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_workspace.py +19 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +18 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_rack_module.py +53 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_resolution_proposal_api.py +4 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +173 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_source_trace_contract.py +66 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_standard_views.py +24 -2
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_target_field_catalog.py +4 -4
- netbox_data_import-2.4.0/netbox_data_import/tests/test_target_module_edge_contracts.py +1069 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_trace_device_resolution.py +843 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_workbook.py +0 -29
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_workspace.py +161 -14
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views.py +113 -56
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views_coverage2.py +12 -21
- netbox_data_import-2.4.0/netbox_data_import/trace_device_resolution.py +482 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/trace_workbook.py +19 -104
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/urls.py +11 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/views.py +459 -453
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/SOURCES.txt +17 -1
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/pyproject.toml +6 -2
- netbox_data_import-2.2.0/netbox_data_import/graphql/schema.py +0 -22
- netbox_data_import-2.2.0/netbox_data_import/graphql/types.py +0 -23
- netbox_data_import-2.2.0/netbox_data_import/inference_connection_test.py +0 -73
- netbox_data_import-2.2.0/netbox_data_import/inference_transport.py +0 -130
- netbox_data_import-2.2.0/netbox_data_import/object_permissions.py +0 -340
- netbox_data_import-2.2.0/netbox_data_import/tests/test_inference_connection_test.py +0 -452
- netbox_data_import-2.2.0/netbox_data_import/tests/test_inference_secret_containment.py +0 -216
- netbox_data_import-2.2.0/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -171
- netbox_data_import-2.2.0/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -449
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/LICENSE +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/README.md +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/adapter_config.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/adapter_forms.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/__init__.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/contact_resolution.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/device_field_review.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/device_identity.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/field_keys.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/filters.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/flat_workbook.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/__init__.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/filters.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_trust.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0001_initial.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0004_features.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/__init__.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/navigation.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/plan.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/preview_row_actions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_decisions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_presentation.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_tasks.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/resolution_proposals.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/signals.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/source_resolution.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tables.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/target_runtime.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/template_content.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_proposal_card.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/__init__.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/conftest.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/isolated_settings.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/mixins.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/parallel.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_api.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_comment_style.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_plan.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_ip_target.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_models.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_navigation.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_decisions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_resolution_proposals.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_actions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_resolutions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_serial_display.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views_extra.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/trace_schema.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/transform_regex.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/utils.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/values.py +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/requires.txt +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/top_level.txt +0 -0
- {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/setup.cfg +0 -0
|
@@ -13,13 +13,14 @@ the NetBox boundary.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from dataclasses import dataclass, field
|
|
16
|
+
from dataclasses import dataclass, field, replace
|
|
17
17
|
from typing import TYPE_CHECKING
|
|
18
18
|
|
|
19
19
|
from .catalog import OutputKind, has_implemented_module
|
|
20
|
+
from .values import identity_text
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
|
-
from .
|
|
23
|
+
from .source_trace import SourceTrace
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class UnknownSourceAdapter(Exception):
|
|
@@ -39,6 +40,16 @@ class SourceDiagnostic:
|
|
|
39
40
|
row_number: int | None = None
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
def _foreign_row_number(row) -> int | None:
|
|
44
|
+
"""Return the source row a foreign row states, so its diagnostic can name a location."""
|
|
45
|
+
provenance = getattr(row, "provenance", ())
|
|
46
|
+
if provenance:
|
|
47
|
+
return provenance[0].row_start
|
|
48
|
+
if isinstance(row, dict):
|
|
49
|
+
return row.get("_row_number")
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
@dataclass(frozen=True)
|
|
43
54
|
class SourceBatch:
|
|
44
55
|
"""The typed source items and source diagnostics from one file (section 1)."""
|
|
@@ -48,6 +59,46 @@ class SourceBatch:
|
|
|
48
59
|
diagnostics: tuple[SourceDiagnostic, ...] = ()
|
|
49
60
|
unused_columns: dict[str, dict] = field(default_factory=dict)
|
|
50
61
|
|
|
62
|
+
def __post_init__(self):
|
|
63
|
+
"""Exclude a row the output kinds cannot carry, and mark a Trace with empty Device references."""
|
|
64
|
+
from .source_trace import SourceTrace
|
|
65
|
+
|
|
66
|
+
trace_batch = OutputKind.SOURCE_TRACE in self.output_kinds
|
|
67
|
+
expected = SourceTrace if trace_batch else dict
|
|
68
|
+
carries = "Source Traces" if trace_batch else "source rows"
|
|
69
|
+
rows = []
|
|
70
|
+
diagnostics = list(self.diagnostics)
|
|
71
|
+
for row in self.rows:
|
|
72
|
+
if not isinstance(row, expected):
|
|
73
|
+
# Excluded here, because every reader below reads the row the output kinds declare.
|
|
74
|
+
diagnostics.append(
|
|
75
|
+
SourceDiagnostic(
|
|
76
|
+
code="source.row_type_unexpected",
|
|
77
|
+
message=f"This batch carries {carries} only; got {type(row).__name__}.",
|
|
78
|
+
row_number=_foreign_row_number(row),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
if not trace_batch:
|
|
83
|
+
rows.append(row)
|
|
84
|
+
continue
|
|
85
|
+
summary = row.endpoint_summary
|
|
86
|
+
references = [summary.from_termination, summary.to_termination, *row.corroboration]
|
|
87
|
+
for segment in row.segments:
|
|
88
|
+
references.extend((segment.left, segment.right))
|
|
89
|
+
if all(identity_text(reference.device) for reference in references):
|
|
90
|
+
rows.append(row)
|
|
91
|
+
continue
|
|
92
|
+
diagnostic = SourceDiagnostic(
|
|
93
|
+
code="trace.device_required",
|
|
94
|
+
message="A Source Trace Termination Reference must name a Device.",
|
|
95
|
+
row_number=row.provenance[0].row_start,
|
|
96
|
+
)
|
|
97
|
+
diagnostics.append(diagnostic)
|
|
98
|
+
rows.append(replace(row, errors=(*row.errors, diagnostic)))
|
|
99
|
+
object.__setattr__(self, "rows", tuple(rows))
|
|
100
|
+
object.__setattr__(self, "diagnostics", tuple(diagnostics))
|
|
101
|
+
|
|
51
102
|
|
|
52
103
|
class SourceAdapter:
|
|
53
104
|
"""Base class for a Source Adapter declaration."""
|
|
@@ -10,8 +10,11 @@ from rest_framework import serializers
|
|
|
10
10
|
|
|
11
11
|
from ..adapters import DEFAULT_ADAPTER_KEY, get_adapter, selectable_adapter_choices
|
|
12
12
|
from ..catalog import CATALOG
|
|
13
|
+
from ..forms import InferenceBackendForm
|
|
13
14
|
from ..models import (
|
|
14
15
|
ImportProfile,
|
|
16
|
+
CableClassMapping,
|
|
17
|
+
CableImportSource,
|
|
15
18
|
ColumnMapping,
|
|
16
19
|
ClassRoleMapping,
|
|
17
20
|
DeviceTypeMapping,
|
|
@@ -63,6 +66,17 @@ class PolicySectionSerializer(PolicySectionApplicabilityMixin, ValidatedModelSer
|
|
|
63
66
|
def validate_policy_row(self, attrs):
|
|
64
67
|
"""Check the fields this model resolves through the catalog. Subclasses override."""
|
|
65
68
|
|
|
69
|
+
def model_cleaned_values(self):
|
|
70
|
+
"""Return request values after applying the model's canonical normalization."""
|
|
71
|
+
values = dict(self.validated_data)
|
|
72
|
+
if self.instance is None:
|
|
73
|
+
instance = self.Meta.model(**values)
|
|
74
|
+
instance.full_clean(validate_unique=False)
|
|
75
|
+
else:
|
|
76
|
+
# ValidatedModelSerializer.validate() applies request values and full_clean() to this instance.
|
|
77
|
+
instance = self.instance
|
|
78
|
+
return {name: getattr(instance, name) for name in values}
|
|
79
|
+
|
|
66
80
|
|
|
67
81
|
def _validate_target_keys(instance, attrs, names, *, allow_candidates=True, required=False):
|
|
68
82
|
"""Reject a target key the profile's Source Adapter cannot supply."""
|
|
@@ -145,6 +159,24 @@ class ColumnMappingSerializer(PolicySectionSerializer):
|
|
|
145
159
|
_validate_target_keys(self.instance, attrs, ("target_field",), required=True)
|
|
146
160
|
|
|
147
161
|
|
|
162
|
+
class CableClassMappingSerializer(PolicySectionSerializer):
|
|
163
|
+
"""Serializer for CableClassMapping policy decisions."""
|
|
164
|
+
|
|
165
|
+
class Meta:
|
|
166
|
+
model = CableClassMapping
|
|
167
|
+
fields = [
|
|
168
|
+
"id",
|
|
169
|
+
"url",
|
|
170
|
+
"display",
|
|
171
|
+
"profile",
|
|
172
|
+
"cable_class",
|
|
173
|
+
"cable_type_resolved",
|
|
174
|
+
"cable_type",
|
|
175
|
+
"cable_profile_resolved",
|
|
176
|
+
"cable_profile",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
148
180
|
class _RackTypeSlugField(serializers.SlugRelatedField):
|
|
149
181
|
"""SlugRelatedField for RackType that defers the queryset import."""
|
|
150
182
|
|
|
@@ -282,6 +314,30 @@ class ImportExecutionSerializer(serializers.ModelSerializer):
|
|
|
282
314
|
read_only_fields = fields
|
|
283
315
|
|
|
284
316
|
|
|
317
|
+
class CableImportSourceSerializer(serializers.ModelSerializer):
|
|
318
|
+
"""Read-only serializer for one Cable provenance record."""
|
|
319
|
+
|
|
320
|
+
class Meta:
|
|
321
|
+
model = CableImportSource
|
|
322
|
+
fields = [
|
|
323
|
+
"id",
|
|
324
|
+
"cable",
|
|
325
|
+
"profile",
|
|
326
|
+
"trace_identity",
|
|
327
|
+
"segment_index",
|
|
328
|
+
"from_text",
|
|
329
|
+
"to_text",
|
|
330
|
+
"direction",
|
|
331
|
+
"workbook_fingerprint",
|
|
332
|
+
"sheet",
|
|
333
|
+
"block_ordinal",
|
|
334
|
+
"row_start",
|
|
335
|
+
"row_end",
|
|
336
|
+
"export_timestamp",
|
|
337
|
+
]
|
|
338
|
+
read_only_fields = fields
|
|
339
|
+
|
|
340
|
+
|
|
285
341
|
class ResolutionProposalSerializer(serializers.ModelSerializer):
|
|
286
342
|
"""Read-only serializer for the Resolution Proposal audit record."""
|
|
287
343
|
|
|
@@ -328,28 +384,20 @@ class ResolutionProposalHistorySerializer(serializers.ModelSerializer):
|
|
|
328
384
|
|
|
329
385
|
|
|
330
386
|
class InferenceBackendSerializer(NetBoxModelSerializer):
|
|
331
|
-
"""Serialize one Inference Backend row
|
|
387
|
+
"""Serialize one Inference Backend row with a write-only credential reference.
|
|
332
388
|
|
|
333
389
|
NetBox resolves this class by model name to freeze a delete event payload.
|
|
334
390
|
"""
|
|
335
391
|
|
|
392
|
+
credential_reference = serializers.JSONField(write_only=True)
|
|
393
|
+
|
|
336
394
|
class Meta:
|
|
337
395
|
model = InferenceBackend
|
|
338
396
|
fields = [
|
|
339
397
|
"id",
|
|
340
398
|
"url",
|
|
341
399
|
"display",
|
|
342
|
-
|
|
343
|
-
"display_name",
|
|
344
|
-
"adapter_type",
|
|
345
|
-
"api_root",
|
|
346
|
-
"model",
|
|
347
|
-
"authentication",
|
|
348
|
-
"response_mode",
|
|
349
|
-
"connect_timeout",
|
|
350
|
-
"read_timeout",
|
|
351
|
-
"enabled",
|
|
352
|
-
"tags",
|
|
400
|
+
*InferenceBackendForm.Meta.fields,
|
|
353
401
|
"custom_fields",
|
|
354
402
|
"created",
|
|
355
403
|
"last_updated",
|
|
@@ -7,6 +7,8 @@ from netbox.api.routers import NetBoxRouter
|
|
|
7
7
|
|
|
8
8
|
from .views import (
|
|
9
9
|
ImportProfileViewSet,
|
|
10
|
+
CableClassMappingViewSet,
|
|
11
|
+
CableImportSourceViewSet,
|
|
10
12
|
ColumnMappingViewSet,
|
|
11
13
|
ClassRoleMappingViewSet,
|
|
12
14
|
DeviceTypeMappingViewSet,
|
|
@@ -21,6 +23,8 @@ from .views import (
|
|
|
21
23
|
|
|
22
24
|
router = NetBoxRouter()
|
|
23
25
|
router.register("profiles", ImportProfileViewSet)
|
|
26
|
+
router.register("cable-class-mappings", CableClassMappingViewSet)
|
|
27
|
+
router.register("cable-provenance", CableImportSourceViewSet)
|
|
24
28
|
router.register("column-mappings", ColumnMappingViewSet)
|
|
25
29
|
router.register("class-role-mappings", ClassRoleMappingViewSet)
|
|
26
30
|
router.register("device-type-mappings", DeviceTypeMappingViewSet)
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
"""DRF viewsets for the data-import plugin API."""
|
|
4
4
|
|
|
5
5
|
from django.http import Http404
|
|
6
|
-
from netbox.api.viewsets import NetBoxModelViewSet
|
|
6
|
+
from netbox.api.viewsets import NetBoxModelViewSet
|
|
7
7
|
from rest_framework import mixins, permissions, viewsets
|
|
8
|
-
from rest_framework.exceptions import ValidationError
|
|
8
|
+
from rest_framework.exceptions import PermissionDenied, ValidationError
|
|
9
9
|
from rest_framework.permissions import DjangoModelPermissions
|
|
10
10
|
|
|
11
11
|
from ..field_keys import SELECT_TERMINATION_TASK, parse_termination_field_key
|
|
12
12
|
from ..models import (
|
|
13
13
|
locked_profile_policy,
|
|
14
|
-
locked_resolution_policy,
|
|
15
14
|
ImportProfile,
|
|
15
|
+
CableClassMapping,
|
|
16
|
+
CableImportSource,
|
|
16
17
|
ColumnMapping,
|
|
17
18
|
ClassRoleMapping,
|
|
18
19
|
DeviceTypeMapping,
|
|
@@ -23,8 +24,15 @@ from ..models import (
|
|
|
23
24
|
InferenceBackend,
|
|
24
25
|
ResolutionProposal,
|
|
25
26
|
)
|
|
27
|
+
from ..object_permissions import (
|
|
28
|
+
ObjectPermissionDenied,
|
|
29
|
+
delete_permission_scoped_objects,
|
|
30
|
+
save_permission_scoped_object,
|
|
31
|
+
)
|
|
26
32
|
from .serializers import (
|
|
27
33
|
ImportProfileSerializer,
|
|
34
|
+
CableClassMappingSerializer,
|
|
35
|
+
CableImportSourceSerializer,
|
|
28
36
|
ColumnMappingSerializer,
|
|
29
37
|
ClassRoleMappingSerializer,
|
|
30
38
|
DeviceTypeMappingSerializer,
|
|
@@ -84,6 +92,72 @@ class _PluginModelViewSet(_ProfileScopedQuerySetMixin, viewsets.ModelViewSet):
|
|
|
84
92
|
|
|
85
93
|
permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
|
|
86
94
|
|
|
95
|
+
def perform_create(self, serializer):
|
|
96
|
+
"""Create one policy row inside its profile and object-permission scope."""
|
|
97
|
+
model = serializer.Meta.model
|
|
98
|
+
profile_id = serializer.validated_data["profile"].pk
|
|
99
|
+
try:
|
|
100
|
+
with locked_profile_policy(profile_id):
|
|
101
|
+
serializer.run_validation(serializer.initial_data)
|
|
102
|
+
values = serializer.model_cleaned_values()
|
|
103
|
+
profile = values.pop("profile")
|
|
104
|
+
result = save_permission_scoped_object(
|
|
105
|
+
self.request.user,
|
|
106
|
+
model,
|
|
107
|
+
{"pk": None, "profile": profile},
|
|
108
|
+
values,
|
|
109
|
+
on_existing="reject",
|
|
110
|
+
)
|
|
111
|
+
except ImportProfile.DoesNotExist:
|
|
112
|
+
raise Http404 from None
|
|
113
|
+
except ObjectPermissionDenied as exc:
|
|
114
|
+
raise PermissionDenied from exc
|
|
115
|
+
serializer.instance = result.instance
|
|
116
|
+
|
|
117
|
+
def perform_update(self, serializer):
|
|
118
|
+
"""Update one policy row under every affected profile and object scope."""
|
|
119
|
+
model = serializer.Meta.model
|
|
120
|
+
row_pk = serializer.instance.pk
|
|
121
|
+
stored_profile_id = model.objects.filter(pk=row_pk).values_list("profile_id", flat=True).first()
|
|
122
|
+
if stored_profile_id is None:
|
|
123
|
+
raise Http404
|
|
124
|
+
requested_profile = serializer.validated_data.get("profile")
|
|
125
|
+
requested_profile_id = requested_profile.pk if requested_profile is not None else stored_profile_id
|
|
126
|
+
try:
|
|
127
|
+
with locked_profile_policy(stored_profile_id, requested_profile_id):
|
|
128
|
+
serializer.instance = model.objects.get(pk=row_pk, profile_id=stored_profile_id)
|
|
129
|
+
serializer.run_validation(serializer.initial_data)
|
|
130
|
+
result = save_permission_scoped_object(
|
|
131
|
+
self.request.user,
|
|
132
|
+
model,
|
|
133
|
+
{"pk": row_pk, "profile_id": stored_profile_id},
|
|
134
|
+
serializer.model_cleaned_values(),
|
|
135
|
+
)
|
|
136
|
+
except (model.DoesNotExist, ImportProfile.DoesNotExist):
|
|
137
|
+
raise Http404 from None
|
|
138
|
+
except ObjectPermissionDenied as exc:
|
|
139
|
+
raise PermissionDenied from exc
|
|
140
|
+
serializer.instance = result.instance
|
|
141
|
+
|
|
142
|
+
def perform_destroy(self, instance):
|
|
143
|
+
"""Delete one policy row under its current profile and object scope."""
|
|
144
|
+
model = type(instance)
|
|
145
|
+
profile_id = model.objects.filter(pk=instance.pk).values_list("profile_id", flat=True).first()
|
|
146
|
+
if profile_id is None:
|
|
147
|
+
raise Http404
|
|
148
|
+
try:
|
|
149
|
+
with locked_profile_policy(profile_id):
|
|
150
|
+
deleted = delete_permission_scoped_objects(
|
|
151
|
+
self.request.user,
|
|
152
|
+
model.objects.filter(pk=instance.pk, profile_id=profile_id),
|
|
153
|
+
)
|
|
154
|
+
if deleted != 1:
|
|
155
|
+
raise Http404
|
|
156
|
+
except ImportProfile.DoesNotExist:
|
|
157
|
+
raise Http404 from None
|
|
158
|
+
except ObjectPermissionDenied as exc:
|
|
159
|
+
raise PermissionDenied from exc
|
|
160
|
+
|
|
87
161
|
|
|
88
162
|
class ColumnMappingViewSet(_PluginModelViewSet):
|
|
89
163
|
"""CRUD viewset for ColumnMapping."""
|
|
@@ -92,6 +166,13 @@ class ColumnMappingViewSet(_PluginModelViewSet):
|
|
|
92
166
|
serializer_class = ColumnMappingSerializer
|
|
93
167
|
|
|
94
168
|
|
|
169
|
+
class CableClassMappingViewSet(_PluginModelViewSet):
|
|
170
|
+
"""CRUD viewset for CableClassMapping."""
|
|
171
|
+
|
|
172
|
+
queryset = CableClassMapping.objects.select_related("profile")
|
|
173
|
+
serializer_class = CableClassMappingSerializer
|
|
174
|
+
|
|
175
|
+
|
|
95
176
|
class ClassRoleMappingViewSet(_PluginModelViewSet):
|
|
96
177
|
"""CRUD viewset for ClassRoleMapping."""
|
|
97
178
|
|
|
@@ -120,53 +201,12 @@ class ColumnTransformRuleViewSet(_PluginModelViewSet):
|
|
|
120
201
|
serializer_class = ColumnTransformRuleSerializer
|
|
121
202
|
|
|
122
203
|
|
|
123
|
-
def _revalidate_against_the_stored_row(serializer):
|
|
124
|
-
"""Read the resolution again and check the request against the row as it now stands.
|
|
125
|
-
|
|
126
|
-
save() writes every field, so a request that changed another field first would otherwise be
|
|
127
|
-
undone. The whole validation runs again rather than validate() alone, because the field checks
|
|
128
|
-
read the stored row too, and the profile lock makes this reading of it authoritative. The
|
|
129
|
-
result is discarded: the values are the request's own, which the first pass already holds.
|
|
130
|
-
"""
|
|
131
|
-
serializer.instance = SourceResolution.objects.get(pk=serializer.instance.pk)
|
|
132
|
-
serializer.run_validation(serializer.initial_data)
|
|
133
|
-
|
|
134
|
-
|
|
135
204
|
class SourceResolutionViewSet(_PluginModelViewSet):
|
|
136
205
|
"""CRUD viewset for SourceResolution (rerere)."""
|
|
137
206
|
|
|
138
207
|
queryset = SourceResolution.objects.select_related("profile")
|
|
139
208
|
serializer_class = SourceResolutionSerializer
|
|
140
209
|
|
|
141
|
-
# Each write serializes against an executing import, which holds the same profile row.
|
|
142
|
-
def perform_create(self, serializer):
|
|
143
|
-
"""Create the resolution under the profile lock."""
|
|
144
|
-
try:
|
|
145
|
-
with locked_profile_policy(serializer.validated_data["profile"].pk):
|
|
146
|
-
serializer.save()
|
|
147
|
-
except ImportProfile.DoesNotExist:
|
|
148
|
-
# The profile is read to validate the request, and can be deleted before the lock.
|
|
149
|
-
raise Http404 from None
|
|
150
|
-
|
|
151
|
-
def perform_update(self, serializer):
|
|
152
|
-
"""Update the resolution under its profile lock."""
|
|
153
|
-
# ValidatedModelSerializer.validate() writes the request values onto the instance, so only
|
|
154
|
-
# its primary key still names the stored row.
|
|
155
|
-
try:
|
|
156
|
-
with locked_resolution_policy(serializer.instance.pk):
|
|
157
|
-
_revalidate_against_the_stored_row(serializer)
|
|
158
|
-
serializer.save()
|
|
159
|
-
except (SourceResolution.DoesNotExist, ImportProfile.DoesNotExist):
|
|
160
|
-
raise Http404 from None
|
|
161
|
-
|
|
162
|
-
def perform_destroy(self, instance):
|
|
163
|
-
"""Delete the resolution under its profile lock."""
|
|
164
|
-
try:
|
|
165
|
-
with locked_resolution_policy(instance.pk):
|
|
166
|
-
instance.delete()
|
|
167
|
-
except (SourceResolution.DoesNotExist, ImportProfile.DoesNotExist):
|
|
168
|
-
raise Http404 from None
|
|
169
|
-
|
|
170
210
|
|
|
171
211
|
class ImportExecutionViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
|
|
172
212
|
"""Read-only viewset for the Import Execution audit history."""
|
|
@@ -176,6 +216,27 @@ class ImportExecutionViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModel
|
|
|
176
216
|
permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
|
|
177
217
|
|
|
178
218
|
|
|
219
|
+
class CableImportSourceViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
|
|
220
|
+
"""Read-only viewset for per-Cable import provenance."""
|
|
221
|
+
|
|
222
|
+
queryset = CableImportSource.objects.select_related("cable", "profile")
|
|
223
|
+
serializer_class = CableImportSourceSerializer
|
|
224
|
+
permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
|
|
225
|
+
|
|
226
|
+
def get_queryset(self):
|
|
227
|
+
"""Apply the profile scope and an optional Cable ID filter."""
|
|
228
|
+
profile_scope = ImportProfile.objects.restrict(self.request.user, "view")
|
|
229
|
+
qs = super().get_queryset().filter(profile__in=profile_scope)
|
|
230
|
+
cable_id = self.request.query_params.get("cable_id")
|
|
231
|
+
if cable_id is not None:
|
|
232
|
+
try:
|
|
233
|
+
cable_id = int(cable_id)
|
|
234
|
+
except (TypeError, ValueError) as exc:
|
|
235
|
+
raise ValidationError({"cable_id": "Enter a whole number."}) from exc
|
|
236
|
+
qs = qs.filter(cable_id=cable_id)
|
|
237
|
+
return qs
|
|
238
|
+
|
|
239
|
+
|
|
179
240
|
class ResolutionProposalViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
|
|
180
241
|
"""Read-only viewset for Resolution Proposal history."""
|
|
181
242
|
|
|
@@ -225,12 +286,8 @@ class ResolutionProposalHistoryViewSet(mixins.ListModelMixin, viewsets.GenericVi
|
|
|
225
286
|
)
|
|
226
287
|
|
|
227
288
|
|
|
228
|
-
class InferenceBackendViewSet(
|
|
229
|
-
"""
|
|
230
|
-
|
|
231
|
-
Read-only on purpose: a backend row carries the destination NetBox itself calls, and the UI form
|
|
232
|
-
is the one place that validates the `api_root` trust boundary against the allowlist.
|
|
233
|
-
"""
|
|
289
|
+
class InferenceBackendViewSet(NetBoxModelViewSet):
|
|
290
|
+
"""CRUD viewset whose configuration fields derive from the Inference Backend form."""
|
|
234
291
|
|
|
235
292
|
queryset = InferenceBackend.objects.prefetch_related("tags")
|
|
236
293
|
serializer_class = InferenceBackendSerializer
|
|
@@ -36,6 +36,13 @@ from .field_keys import (
|
|
|
36
36
|
from .object_permissions import enforce_saved_object_permission
|
|
37
37
|
from .plan import Diagnostic, Disposition, PlannedChange, Severity, SynchronizationUnit
|
|
38
38
|
from .target_runtime import DeletedObject, PreconditionFailed
|
|
39
|
+
from .trace_device_resolution import (
|
|
40
|
+
STALE as DEVICE_STALE,
|
|
41
|
+
collect_trace_device_evidence,
|
|
42
|
+
resolve_trace_devices,
|
|
43
|
+
resolved_trace_device,
|
|
44
|
+
source_device_key,
|
|
45
|
+
)
|
|
39
46
|
from .values import identity_text, source_text
|
|
40
47
|
|
|
41
48
|
CABLE_STATUS = "connected"
|
|
@@ -207,7 +214,9 @@ class _TraceAnalysis:
|
|
|
207
214
|
segments: list = field(default_factory=list)
|
|
208
215
|
proven: dict = field(default_factory=dict)
|
|
209
216
|
policies: dict = field(default_factory=dict)
|
|
217
|
+
devices: dict = field(default_factory=dict)
|
|
210
218
|
terminations: dict = field(default_factory=dict)
|
|
219
|
+
resolution_started: bool = False
|
|
211
220
|
topology_read: bool = False
|
|
212
221
|
logical_cable: Any = None
|
|
213
222
|
retains_logical_cable: bool = False
|
|
@@ -303,38 +312,19 @@ def _source_record(trace, segment_index: int) -> dict:
|
|
|
303
312
|
}
|
|
304
313
|
|
|
305
314
|
|
|
306
|
-
def
|
|
307
|
-
"""Return the visible Devices at the import target, grouped by comparison name."""
|
|
308
|
-
from django.db.models import Q
|
|
309
|
-
|
|
310
|
-
wanted = {identity_text(name) for name in names if identity_text(name)}
|
|
311
|
-
if not wanted:
|
|
312
|
-
return {}
|
|
313
|
-
devices = netbox_reader.devices()
|
|
314
|
-
if netbox_reader.site is not None:
|
|
315
|
-
devices = devices.filter(site=netbox_reader.site)
|
|
316
|
-
if _lock_rows:
|
|
317
|
-
devices = devices.order_by("pk").select_for_update(of=("self",))
|
|
318
|
-
lookup = Q()
|
|
319
|
-
for name in sorted(wanted):
|
|
320
|
-
lookup |= Q(name__iexact=name)
|
|
321
|
-
grouped: dict[str, list] = {}
|
|
322
|
-
for device in devices.filter(lookup):
|
|
323
|
-
comparison = identity_text(device.name)
|
|
324
|
-
if comparison in wanted:
|
|
325
|
-
grouped.setdefault(comparison, []).append(device)
|
|
326
|
-
return grouped
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def resolved_device_for(field_key: str, netbox_reader, *, _lock_rows=False):
|
|
315
|
+
def resolved_device_for(field_key: str, netbox_reader, *, profile, _lock_rows=False):
|
|
330
316
|
"""Return the one visible Device a termination field key names, or None when it names no single one.
|
|
331
317
|
|
|
332
318
|
A proposal freezes this reference and revalidates it, so the retrieval and the freshness check
|
|
333
319
|
have to agree on what "the resolved Device" means.
|
|
334
320
|
"""
|
|
335
321
|
device_name = parse_termination_field_key(field_key)["device"]
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
return resolved_trace_device(
|
|
323
|
+
profile=profile,
|
|
324
|
+
reader=netbox_reader,
|
|
325
|
+
source_label=device_name,
|
|
326
|
+
lock_rows=_lock_rows,
|
|
327
|
+
)
|
|
338
328
|
|
|
339
329
|
|
|
340
330
|
_RESOLVED_DEVICE_UNSET = object()
|
|
@@ -361,7 +351,7 @@ def eligible_terminations(
|
|
|
361
351
|
kind = parsed["kind"]
|
|
362
352
|
accessor = _READER_ACCESSOR_BY_KIND[kind]
|
|
363
353
|
device = (
|
|
364
|
-
resolved_device_for(field_key, netbox_reader, _lock_rows=_lock_rows)
|
|
354
|
+
resolved_device_for(field_key, netbox_reader, profile=profile, _lock_rows=_lock_rows)
|
|
365
355
|
if _resolved_device is _RESOLVED_DEVICE_UNSET
|
|
366
356
|
else _resolved_device
|
|
367
357
|
)
|
|
@@ -430,6 +420,7 @@ class _CableBatch:
|
|
|
430
420
|
"""Plan every Source Trace in one batch, so an identical shared segment plans once."""
|
|
431
421
|
|
|
432
422
|
def __init__(self, traces, profile, netbox_reader, *, lock_plan_references: bool = False):
|
|
423
|
+
traces = tuple(traces)
|
|
433
424
|
self.profile = profile
|
|
434
425
|
self.reader = netbox_reader
|
|
435
426
|
self.actor = netbox_reader.actor
|
|
@@ -445,7 +436,14 @@ class _CableBatch:
|
|
|
445
436
|
self._occupied: dict[tuple[str, int], _ExistingCable] = {}
|
|
446
437
|
self._mapping_rows: dict[str, Any] | None = None
|
|
447
438
|
self._stored = self._stored_resolutions()
|
|
448
|
-
|
|
439
|
+
active_traces = (analysis.trace for analysis in self.analyses if not analysis.stopped)
|
|
440
|
+
self._device_evidence = collect_trace_device_evidence(active_traces)
|
|
441
|
+
self._device_resolutions = resolve_trace_devices(
|
|
442
|
+
profile=self.profile,
|
|
443
|
+
reader=self.reader,
|
|
444
|
+
evidence=self._device_evidence,
|
|
445
|
+
lock_rows=self.lock_plan_references,
|
|
446
|
+
)
|
|
449
447
|
self._resolve_terminations()
|
|
450
448
|
self._load_mappings()
|
|
451
449
|
self._build_segments()
|
|
@@ -512,16 +510,6 @@ class _CableBatch:
|
|
|
512
510
|
).select_related("selected_object_type")
|
|
513
511
|
return {row.field_key: row for row in rows}
|
|
514
512
|
|
|
515
|
-
def _load_devices(self) -> dict[str, list]:
|
|
516
|
-
"""Return the visible Devices every planned trace names, grouped by comparison name."""
|
|
517
|
-
names = [
|
|
518
|
-
reference.device
|
|
519
|
-
for analysis in self.analyses
|
|
520
|
-
if not analysis.stopped
|
|
521
|
-
for reference in self._references(analysis.trace)
|
|
522
|
-
]
|
|
523
|
-
return _visible_devices(self.reader, names)
|
|
524
|
-
|
|
525
513
|
def _components_for(self, device_id: int, kind: str) -> dict[str, list]:
|
|
526
514
|
"""Return one Device's terminations of one kind, grouped by comparison name."""
|
|
527
515
|
cached = self._components.get((device_id, kind))
|
|
@@ -537,6 +525,7 @@ class _CableBatch:
|
|
|
537
525
|
for analysis in self.analyses:
|
|
538
526
|
if analysis.stopped:
|
|
539
527
|
continue
|
|
528
|
+
analysis.resolution_started = True
|
|
540
529
|
resolved: dict[tuple, _Termination] = {}
|
|
541
530
|
for reference in self._references(analysis.trace):
|
|
542
531
|
if reference.identity_key in resolved:
|
|
@@ -548,19 +537,24 @@ class _CableBatch:
|
|
|
548
537
|
|
|
549
538
|
def _resolve_one(self, analysis: _TraceAnalysis, reference) -> _Termination | None:
|
|
550
539
|
"""Return the NetBox object one Termination Reference names, or record the open decision."""
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
540
|
+
resolution = self._device_resolutions[source_device_key(reference.device)]
|
|
541
|
+
analysis.devices.setdefault(resolution.evidence.key, resolution.to_question())
|
|
542
|
+
if resolution.device is None:
|
|
543
|
+
code = "trace.device_resolution_stale" if resolution.state == DEVICE_STALE else "trace.device_unresolved"
|
|
544
|
+
analysis.block(
|
|
545
|
+
code,
|
|
546
|
+
{**_reference_display(reference), "matches": resolution.exact_match_count},
|
|
547
|
+
)
|
|
554
548
|
# With no resolved Device there is nothing to pick from, so the picker cannot help here.
|
|
555
549
|
self._record_resolution(
|
|
556
550
|
analysis,
|
|
557
551
|
reference,
|
|
558
552
|
UNRESOLVED,
|
|
559
553
|
None,
|
|
560
|
-
reason=
|
|
554
|
+
reason="Resolve the source Device before choosing one of its ports.",
|
|
561
555
|
)
|
|
562
556
|
return None
|
|
563
|
-
device =
|
|
557
|
+
device = resolution.device
|
|
564
558
|
stored = self._stored.get(_field_key(reference))
|
|
565
559
|
if stored is not None:
|
|
566
560
|
termination = self._stored_termination(analysis, reference, device, stored)
|
|
@@ -1222,9 +1216,11 @@ class _CableBatch:
|
|
|
1222
1216
|
},
|
|
1223
1217
|
"segments": segments,
|
|
1224
1218
|
"logical_cable": self._logical_cable_display(analysis),
|
|
1219
|
+
"resolution_started": analysis.resolution_started,
|
|
1225
1220
|
"topology_known": analysis.topology_read,
|
|
1226
1221
|
# A unit with no changes proposes nothing, so the panel must not offer to delete one.
|
|
1227
1222
|
"deletes_logical_cable": writes and analysis.deleted_logical_cable is not None,
|
|
1223
|
+
"devices": list(analysis.devices.values()),
|
|
1228
1224
|
"terminations": list(analysis.terminations.values()),
|
|
1229
1225
|
}
|
|
1230
1226
|
|
|
@@ -259,6 +259,7 @@ POLICY_SECTIONS: tuple[PolicySection, ...] = (
|
|
|
259
259
|
PolicySection("source_resolutions", "Source Resolutions", _DEVICE_ONLY),
|
|
260
260
|
PolicySection("device_existing_matches", "Device Existing Matches", _DEVICE_ONLY),
|
|
261
261
|
PolicySection("ignored_field_differences", "Ignored Field Differences", _DEVICE_ONLY),
|
|
262
|
+
PolicySection("trace_device_resolutions", "Trace Device Resolutions", _TRACE_ONLY),
|
|
262
263
|
PolicySection("termination_resolutions", "Termination Resolutions", _TRACE_ONLY),
|
|
263
264
|
PolicySection("cable_class_mappings", "CableClass Mappings", _TRACE_ONLY),
|
|
264
265
|
)
|