netbox-data-import 2.0.0__tar.gz → 2.1.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.0.0 → netbox_data_import-2.1.0}/PKG-INFO +2 -1
- netbox_data_import-2.1.0/netbox_data_import/__init__.py +49 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/serializers.py +7 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/cable_target.py +175 -10
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/field_keys.py +9 -4
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/forms.py +27 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/import_engine.py +3 -4
- netbox_data_import-2.1.0/netbox_data_import/inference_adapter.py +260 -0
- netbox_data_import-2.1.0/netbox_data_import/inference_backend.py +162 -0
- netbox_data_import-2.1.0/netbox_data_import/inference_connection_test.py +73 -0
- netbox_data_import-2.1.0/netbox_data_import/inference_credentials.py +276 -0
- netbox_data_import-2.1.0/netbox_data_import/inference_settings.py +274 -0
- netbox_data_import-2.1.0/netbox_data_import/inference_trust.py +231 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/ip_assignment.py +2 -1
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/jobs.py +24 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0001_initial.py +4 -1
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +1 -1
- netbox_data_import-2.1.0/netbox_data_import/migrations/0031_inferencebackend.py +52 -0
- netbox_data_import-2.1.0/netbox_data_import/migrations/0032_inference_timeout_floor.py +24 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/models.py +88 -8
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/navigation.py +18 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/plan.py +2 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/preview_row_actions.py +77 -3
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/review_workspace.py +189 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/source_resolution.py +7 -5
- netbox_data_import-2.1.0/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +147 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tables.py +14 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/target_modules.py +9 -9
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +8 -0
- netbox_data_import-2.1.0/netbox_data_import/templates/netbox_data_import/inferencebackend.html +95 -0
- netbox_data_import-2.1.0/netbox_data_import/templates/netbox_data_import/trace_workspace.html +287 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/helpers.py +42 -3
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/isolated_settings.py +2 -2
- netbox_data_import-2.1.0/netbox_data_import/tests/mock_discipline.py +560 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_adapter_config_migration.py +4 -4
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_cable_module.py +258 -4
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +5 -4
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_devcontainer_e2e.py +2 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_device_module.py +3 -3
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_execution_audit.py +2 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_cutover.py +25 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_plan.py +1 -1
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_adapter.py +432 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_backend.py +325 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_connection_test.py +452 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_credentials.py +403 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_secret_containment.py +202 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_settings.py +482 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_trust.py +255 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_migrations.py +111 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_mock_discipline.py +865 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_module_boundaries.py +1 -1
- netbox_data_import-2.1.0/netbox_data_import/tests/test_navigation.py +121 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_placement_reference_locks.py +3 -3
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_preview_assets.py +2 -2
- netbox_data_import-2.1.0/netbox_data_import/tests/test_preview_state_ownership.py +254 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +7 -7
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +2 -3
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_review_actions.py +3 -3
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_review_resolutions.py +2 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_standard_views.py +2 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +1 -1
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_trace_workbook.py +29 -0
- netbox_data_import-2.1.0/netbox_data_import/tests/test_trace_workspace.py +1662 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_views.py +69 -17
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_views_coverage2.py +10 -4
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/trace_workbook.py +14 -8
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/urls.py +32 -1
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/views.py +544 -71
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/PKG-INFO +2 -1
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/SOURCES.txt +22 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/requires.txt +1 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/pyproject.toml +45 -4
- netbox_data_import-2.0.0/netbox_data_import/__init__.py +0 -28
- netbox_data_import-2.0.0/netbox_data_import/tests/test_navigation.py +0 -22
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/LICENSE +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/README.md +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/adapter_config.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/adapter_forms.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/adapters.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/__init__.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/urls.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/views.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/catalog.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/contact_resolution.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/device_field_review.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/device_identity.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/filters.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/flat_workbook.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/__init__.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/filters.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/schema.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/types.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0004_features.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/__init__.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/netbox_reader.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/object_permissions.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/signals.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/target_runtime.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/template_content.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/__init__.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/conftest.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/mixins.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/parallel.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_api.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_atomic_exits.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_comment_style.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_contact_sync.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_engine.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_execution.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_ip_target.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_models.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_object_permission_writer.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_pristine_preview_rows.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_rack_module.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_serial_display.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_target_field_catalog.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_views_extra.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/trace_schema.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/transform_regex.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/utils.py +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/values.py +2 -2
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/top_level.txt +0 -0
- {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netbox-data-import
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: NetBox plugin for importing data from external DCIM systems
|
|
5
5
|
Author: Marcin Zieba
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -18,6 +18,7 @@ Requires-Dist: defusedxml>=0.7.1
|
|
|
18
18
|
Requires-Dist: google-re2>=1.1.20251105
|
|
19
19
|
Requires-Dist: openpyxl>=3.1
|
|
20
20
|
Requires-Dist: pyyaml>=6
|
|
21
|
+
Requires-Dist: requests>=2.33.0
|
|
21
22
|
Dynamic: license-file
|
|
22
23
|
|
|
23
24
|
<!--
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from netbox.plugins import PluginConfig
|
|
6
|
+
|
|
7
|
+
__version__ = "2.1.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NetBoxDataImportConfig(PluginConfig):
|
|
11
|
+
"""NetBox plugin configuration for the Data Import plugin."""
|
|
12
|
+
|
|
13
|
+
name = "netbox_data_import"
|
|
14
|
+
verbose_name = "NetBox Data Import"
|
|
15
|
+
description = "NetBox plugin for importing data from external DCIM systems"
|
|
16
|
+
version = __version__
|
|
17
|
+
base_url = "data-import"
|
|
18
|
+
author = "Marcin Zieba"
|
|
19
|
+
author_email = "marcinpsk@gmail.com"
|
|
20
|
+
min_version = "4.6.0"
|
|
21
|
+
graphql_schema = "graphql.schema.schema"
|
|
22
|
+
|
|
23
|
+
default_settings: dict[str, Any] = {
|
|
24
|
+
# A deployment that names no allowlist reaches no origin, rather than every origin.
|
|
25
|
+
"inference_backend_origin_allowlist": [],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def validate(cls, user_config, netbox_version):
|
|
30
|
+
"""Reject a malformed Inference Backend configuration before the application serves a request."""
|
|
31
|
+
super().validate(user_config, netbox_version)
|
|
32
|
+
|
|
33
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
34
|
+
|
|
35
|
+
from .inference_settings import InvalidInferenceConfiguration, validate_plugin_settings
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
validate_plugin_settings(user_config)
|
|
39
|
+
except InvalidInferenceConfiguration as exc:
|
|
40
|
+
raise ImproperlyConfigured(f"Plugin {cls.__module__} has an invalid configuration: {exc}") from exc
|
|
41
|
+
|
|
42
|
+
def ready(self):
|
|
43
|
+
"""Import the jobs module, which NetBox does not load, so its @system_job registration runs."""
|
|
44
|
+
super().ready()
|
|
45
|
+
|
|
46
|
+
from . import jobs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
config = NetBoxDataImportConfig
|
|
@@ -25,6 +25,12 @@ from ..models import (
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _validate_candidate_values(candidate_values):
|
|
29
|
+
"""Require a JSON object at the Contact candidate boundary."""
|
|
30
|
+
if not isinstance(candidate_values, dict):
|
|
31
|
+
raise TypeError("Contact candidate values must be a JSON object.")
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
class PolicySectionApplicabilityMixin:
|
|
29
35
|
"""Apply the shared policy-section applicability rule on the REST write path."""
|
|
30
36
|
|
|
@@ -222,8 +228,7 @@ class SourceResolutionSerializer(PolicySectionSerializer):
|
|
|
222
228
|
resolved_fields = attrs.get("resolved_fields", getattr(instance, "resolved_fields", None))
|
|
223
229
|
try:
|
|
224
230
|
candidate_values = json.loads(original_value)
|
|
225
|
-
|
|
226
|
-
raise ValueError
|
|
231
|
+
_validate_candidate_values(candidate_values)
|
|
227
232
|
configured_sources = profile.column_mappings.filter(target_field="candidate:contact").values_list(
|
|
228
233
|
"source_column", flat=True
|
|
229
234
|
)
|
|
@@ -41,6 +41,23 @@ from .values import identity_text, source_text
|
|
|
41
41
|
CABLE_STATUS = "connected"
|
|
42
42
|
ELIGIBLE_TERMINATION_LIMIT = 20
|
|
43
43
|
|
|
44
|
+
# Section 10.2 badge vocabulary. T8 adds the proposal states to the same set.
|
|
45
|
+
UNRESOLVED = "unresolved"
|
|
46
|
+
AUTOMATICALLY_RESOLVED = "automatically resolved"
|
|
47
|
+
MANUALLY_RESOLVED = "manually resolved"
|
|
48
|
+
|
|
49
|
+
CREATE_SEGMENT = "create"
|
|
50
|
+
REUSE_SEGMENT = "reuse existing"
|
|
51
|
+
CONFLICT_SEGMENT = "conflict"
|
|
52
|
+
_CONFLICT_CODES = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"cable.termination_occupied",
|
|
55
|
+
"cable.multi_termination_conflict",
|
|
56
|
+
"cable.planned_termination_conflict",
|
|
57
|
+
"cable.resolved_segment_conflict",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
44
61
|
_KIND_BY_MODEL_NAME = {
|
|
45
62
|
"interface": INTERFACE_KIND,
|
|
46
63
|
"frontport": FRONT_PORT_KIND,
|
|
@@ -190,7 +207,10 @@ class _TraceAnalysis:
|
|
|
190
207
|
segments: list = field(default_factory=list)
|
|
191
208
|
proven: dict = field(default_factory=dict)
|
|
192
209
|
policies: dict = field(default_factory=dict)
|
|
210
|
+
terminations: dict = field(default_factory=dict)
|
|
211
|
+
topology_read: bool = False
|
|
193
212
|
logical_cable: Any = None
|
|
213
|
+
retains_logical_cable: bool = False
|
|
194
214
|
blocked: bool = False
|
|
195
215
|
invalid: bool = False
|
|
196
216
|
|
|
@@ -204,10 +224,16 @@ class _TraceAnalysis:
|
|
|
204
224
|
"""Return the desired segments no existing Cable already proves."""
|
|
205
225
|
return [segment for segment in self.segments if segment.index not in self.proven]
|
|
206
226
|
|
|
227
|
+
@property
|
|
228
|
+
def deleted_logical_cable(self):
|
|
229
|
+
"""Return the Logical Cable this trace removes, which a Cable that proves its path is not."""
|
|
230
|
+
return None if self.retains_logical_cable else self.logical_cable
|
|
231
|
+
|
|
207
232
|
@property
|
|
208
233
|
def delete_identity(self) -> str | None:
|
|
209
234
|
"""Return the Planned Change identity that removes this trace's Logical Cable."""
|
|
210
|
-
|
|
235
|
+
removed = self.deleted_logical_cable
|
|
236
|
+
return None if removed is None else _delete_identity(removed.cable.pk)
|
|
211
237
|
|
|
212
238
|
def error(self, code: str, display: dict, identities=()) -> None:
|
|
213
239
|
"""Record one blocking or invalidating finding."""
|
|
@@ -502,19 +528,64 @@ class _CableBatch:
|
|
|
502
528
|
devices = self._devices.get(identity_text(reference.device), [])
|
|
503
529
|
if len(devices) != 1:
|
|
504
530
|
analysis.block("trace.device_unresolved", {**_reference_display(reference), "matches": len(devices)})
|
|
531
|
+
# With no resolved Device there is nothing to pick from, so the picker cannot help here.
|
|
532
|
+
self._record_resolution(
|
|
533
|
+
analysis,
|
|
534
|
+
reference,
|
|
535
|
+
UNRESOLVED,
|
|
536
|
+
None,
|
|
537
|
+
reason=f"The source names {len(devices)} matching Devices, so no port list applies.",
|
|
538
|
+
)
|
|
505
539
|
return None
|
|
506
540
|
device = devices[0]
|
|
507
541
|
stored = self._stored.get(_field_key(reference))
|
|
508
542
|
if stored is not None:
|
|
509
|
-
|
|
543
|
+
termination = self._stored_termination(analysis, reference, device, stored)
|
|
544
|
+
state = UNRESOLVED if termination is None else MANUALLY_RESOLVED
|
|
545
|
+
self._record_resolution(analysis, reference, state, termination)
|
|
546
|
+
return termination
|
|
510
547
|
kind = claimed_termination_kind(reference.port_class)
|
|
511
548
|
candidates = self._components_for(device.pk, kind).get(identity_text(reference.port), [])
|
|
512
549
|
if len(candidates) != 1:
|
|
513
550
|
analysis.block(
|
|
514
551
|
"cable.termination_unresolved", {**_reference_display(reference), "matches": len(candidates)}
|
|
515
552
|
)
|
|
553
|
+
self._record_resolution(analysis, reference, UNRESOLVED, None)
|
|
516
554
|
return None
|
|
517
|
-
|
|
555
|
+
termination = self._termination(candidates[0])
|
|
556
|
+
self._record_resolution(analysis, reference, AUTOMATICALLY_RESOLVED, termination)
|
|
557
|
+
return termination
|
|
558
|
+
|
|
559
|
+
@staticmethod
|
|
560
|
+
def _record_resolution(
|
|
561
|
+
analysis: _TraceAnalysis,
|
|
562
|
+
reference,
|
|
563
|
+
state: str,
|
|
564
|
+
termination,
|
|
565
|
+
reason: str = "",
|
|
566
|
+
role: str = TERMINATION_ROLE,
|
|
567
|
+
) -> None:
|
|
568
|
+
"""Record how one Termination Reference was settled, for its badge and its picker."""
|
|
569
|
+
key = _field_key(reference, role)
|
|
570
|
+
claimed = claimed_termination_kind(reference.port_class)
|
|
571
|
+
if role == MAPPED_PEER_ROLE:
|
|
572
|
+
# The picker offers the ports on the far side of the panel, which are the opposite kind.
|
|
573
|
+
kind = REAR_PORT_KIND if claimed == FRONT_PORT_KIND else FRONT_PORT_KIND
|
|
574
|
+
label = f"{_endpoint_label(reference)} (mapped peer)"
|
|
575
|
+
else:
|
|
576
|
+
kind, label = claimed, _endpoint_label(reference)
|
|
577
|
+
analysis.terminations.setdefault(
|
|
578
|
+
key,
|
|
579
|
+
{
|
|
580
|
+
"field_key": key,
|
|
581
|
+
"label": label,
|
|
582
|
+
"kind": kind,
|
|
583
|
+
"state": state,
|
|
584
|
+
"selected": "" if termination is None else termination.display,
|
|
585
|
+
"selectable": not reason,
|
|
586
|
+
"reason": reason,
|
|
587
|
+
},
|
|
588
|
+
)
|
|
518
589
|
|
|
519
590
|
def _stored_termination(self, analysis: _TraceAnalysis, reference, device, stored) -> _Termination | None:
|
|
520
591
|
"""Return the object one saved decision selected, rechecked against current target state."""
|
|
@@ -714,7 +785,9 @@ class _CableBatch:
|
|
|
714
785
|
)
|
|
715
786
|
if selected in peers:
|
|
716
787
|
peer, mapping = peers[selected]
|
|
788
|
+
self._record_resolution(analysis, reference, MANUALLY_RESOLVED, peer, role=MAPPED_PEER_ROLE)
|
|
717
789
|
return self._substituted(analysis, reference, exit_end, peer, mapping)
|
|
790
|
+
self._record_resolution(analysis, reference, UNRESOLVED, None, role=MAPPED_PEER_ROLE)
|
|
718
791
|
analysis.block(
|
|
719
792
|
"cable.ambiguous_mapped_peer",
|
|
720
793
|
{
|
|
@@ -835,6 +908,8 @@ class _CableBatch:
|
|
|
835
908
|
"""
|
|
836
909
|
pending = self._writing()
|
|
837
910
|
for analysis in pending:
|
|
911
|
+
# Only these analyses look at the live topology, so only they may report what is there.
|
|
912
|
+
analysis.topology_read = True
|
|
838
913
|
for segment in analysis.segments:
|
|
839
914
|
candidate = self._occupied.get(segment.left.key)
|
|
840
915
|
proven = candidate if candidate is not None and candidate.satisfies(segment) else None
|
|
@@ -873,7 +948,7 @@ class _CableBatch:
|
|
|
873
948
|
{"segment_index": 0, **cable_display},
|
|
874
949
|
identities=cable_identities,
|
|
875
950
|
)
|
|
876
|
-
analysis.
|
|
951
|
+
analysis.retains_logical_cable = True
|
|
877
952
|
|
|
878
953
|
def _note_reuse(self, analysis: _TraceAnalysis, segment: _DesiredSegment, proven: _ExistingCable) -> None:
|
|
879
954
|
"""Record the proven physical segment, so its live state joins the unit fingerprint."""
|
|
@@ -908,7 +983,8 @@ class _CableBatch:
|
|
|
908
983
|
|
|
909
984
|
def _report_conflicts(self, analysis: _TraceAnalysis) -> None:
|
|
910
985
|
"""Block the trace when a Cable this import may not touch holds a termination it needs."""
|
|
911
|
-
|
|
986
|
+
removed = analysis.deleted_logical_cable
|
|
987
|
+
logical_id = None if removed is None else removed.cable.pk
|
|
912
988
|
for segment in analysis.pending:
|
|
913
989
|
for termination in segment.terminations:
|
|
914
990
|
occupying = self._occupied.get(termination.key)
|
|
@@ -965,7 +1041,7 @@ class _CableBatch:
|
|
|
965
1041
|
if analysis.pending and not self.actor.has_perm("dcim.add_cable"):
|
|
966
1042
|
analysis.block("cable.permission_denied", {"permission": "dcim.add_cable"})
|
|
967
1043
|
return
|
|
968
|
-
logical = analysis.
|
|
1044
|
+
logical = analysis.deleted_logical_cable
|
|
969
1045
|
if logical is not None and not self.actor.has_perm("dcim.delete_cable", logical.cable):
|
|
970
1046
|
cable_display, cable_identities = self._cable_diagnostic_disclosure(logical.cable)
|
|
971
1047
|
analysis.block(
|
|
@@ -1076,16 +1152,99 @@ class _CableBatch:
|
|
|
1076
1152
|
disposition=disposition,
|
|
1077
1153
|
changes=changes,
|
|
1078
1154
|
diagnostics=tuple(analysis.diagnostics),
|
|
1079
|
-
display={
|
|
1155
|
+
display={
|
|
1156
|
+
**analysis.display,
|
|
1157
|
+
"detail": self._detail(analysis, disposition),
|
|
1158
|
+
"trace": self._workspace(analysis, bool(changes)),
|
|
1159
|
+
},
|
|
1080
1160
|
)
|
|
1081
1161
|
|
|
1162
|
+
def _workspace(self, analysis: _TraceAnalysis, writes: bool) -> dict:
|
|
1163
|
+
"""Return what the three review workspace panels show for one Source Trace."""
|
|
1164
|
+
summary = analysis.trace.endpoint_summary
|
|
1165
|
+
resolved = {segment.index: segment for segment in analysis.segments}
|
|
1166
|
+
by_reference = self._resolved.get(analysis.identity, {})
|
|
1167
|
+
conflicted = {
|
|
1168
|
+
diagnostic.display.get("segment_index")
|
|
1169
|
+
for diagnostic in analysis.diagnostics
|
|
1170
|
+
if diagnostic.code in _CONFLICT_CODES
|
|
1171
|
+
}
|
|
1172
|
+
segments = []
|
|
1173
|
+
stated_segments = list(analysis.trace.segments)
|
|
1174
|
+
for index, stated in enumerate(stated_segments):
|
|
1175
|
+
planned = resolved.get(index)
|
|
1176
|
+
source_left, source_right = _endpoint_label(stated.left), _endpoint_label(stated.right)
|
|
1177
|
+
left = source_left if planned is None else planned.left.display
|
|
1178
|
+
right = source_right if planned is None else planned.right.display
|
|
1179
|
+
segments.append(
|
|
1180
|
+
{
|
|
1181
|
+
"index": index,
|
|
1182
|
+
"cable_class": source_text(stated.cable_class),
|
|
1183
|
+
"source_left": source_left,
|
|
1184
|
+
"source_right": source_right,
|
|
1185
|
+
"left": left,
|
|
1186
|
+
"right": right,
|
|
1187
|
+
# A path that re-enters one panel implies a claim the source never states.
|
|
1188
|
+
"pass_through": index > 0 and same_device_and_cards(stated_segments[index - 1].right, stated.left),
|
|
1189
|
+
# Planning substituted the entry port only where the source re-used one port.
|
|
1190
|
+
"substituted": self._entered_through_claim(planned, by_reference.get(stated.left.identity_key)),
|
|
1191
|
+
"status": self._segment_status(analysis, index, conflicted, planned, writes),
|
|
1192
|
+
}
|
|
1193
|
+
)
|
|
1194
|
+
return {
|
|
1195
|
+
"identity": analysis.trace.identity,
|
|
1196
|
+
"endpoints": {
|
|
1197
|
+
"from": _endpoint_label(summary.from_termination),
|
|
1198
|
+
"to": _endpoint_label(summary.to_termination),
|
|
1199
|
+
},
|
|
1200
|
+
"segments": segments,
|
|
1201
|
+
"logical_cable": self._logical_cable_display(analysis),
|
|
1202
|
+
"topology_known": analysis.topology_read,
|
|
1203
|
+
# A unit with no changes proposes nothing, so the panel must not offer to delete one.
|
|
1204
|
+
"deletes_logical_cable": writes and analysis.deleted_logical_cable is not None,
|
|
1205
|
+
"terminations": list(analysis.terminations.values()),
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
@staticmethod
|
|
1209
|
+
def _entered_through_claim(planned: _DesiredSegment | None, stated: _Termination | None) -> bool:
|
|
1210
|
+
"""Return whether planning entered this segment through a port the source never stated."""
|
|
1211
|
+
return planned is not None and stated is not None and planned.left.key != stated.key
|
|
1212
|
+
|
|
1213
|
+
@staticmethod
|
|
1214
|
+
def _segment_status(analysis: _TraceAnalysis, index: int, conflicted: set, planned, writes: bool) -> str:
|
|
1215
|
+
"""Return the verdict the proposed panel shows for one segment, or none for an unplanned one."""
|
|
1216
|
+
if index in conflicted:
|
|
1217
|
+
return CONFLICT_SEGMENT
|
|
1218
|
+
if planned is None:
|
|
1219
|
+
return ""
|
|
1220
|
+
if index in analysis.proven:
|
|
1221
|
+
return REUSE_SEGMENT
|
|
1222
|
+
# A blocked unit contributes no change, so nothing here is going to be created.
|
|
1223
|
+
return CREATE_SEGMENT if writes else ""
|
|
1224
|
+
|
|
1225
|
+
def _logical_cable_display(self, analysis: _TraceAnalysis) -> dict | None:
|
|
1226
|
+
"""Return the Logical Cable NetBox holds for this trace, as far as the actor may see it."""
|
|
1227
|
+
if analysis.logical_cable is None:
|
|
1228
|
+
return None
|
|
1229
|
+
cable = analysis.logical_cable.cable
|
|
1230
|
+
disclosure, _identities = self._cable_diagnostic_disclosure(cable)
|
|
1231
|
+
if not disclosure["cable_visible"]:
|
|
1232
|
+
return {"visible": False, "display": "", "description": "", "tags": []}
|
|
1233
|
+
# Section 6.3 reviews what the deletion removes, which the deletion payload also carries.
|
|
1234
|
+
return {
|
|
1235
|
+
"visible": True,
|
|
1236
|
+
"display": disclosure["cable"],
|
|
1237
|
+
"description": cable.description,
|
|
1238
|
+
"tags": sorted(cable.tags.values_list("name", flat=True)),
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1082
1241
|
def _changes(self, analysis: _TraceAnalysis) -> tuple[PlannedChange, ...]:
|
|
1083
1242
|
"""Return the deletion and the creations one actionable trace performs, in that order."""
|
|
1084
1243
|
if analysis.stopped:
|
|
1085
1244
|
return ()
|
|
1086
1245
|
changes = []
|
|
1087
|
-
if analysis.
|
|
1088
|
-
changes.append(self._delete_change(analysis.
|
|
1246
|
+
if analysis.deleted_logical_cable is not None:
|
|
1247
|
+
changes.append(self._delete_change(analysis.deleted_logical_cable))
|
|
1089
1248
|
changes.extend(self._create_change(segment, analysis.policies[segment.index]) for segment in analysis.pending)
|
|
1090
1249
|
return tuple(changes)
|
|
1091
1250
|
|
|
@@ -1130,7 +1289,7 @@ class _CableBatch:
|
|
|
1130
1289
|
if disposition != Disposition.ACTIONABLE:
|
|
1131
1290
|
return ""
|
|
1132
1291
|
count = len(analysis.pending)
|
|
1133
|
-
if analysis.
|
|
1292
|
+
if analysis.deleted_logical_cable is not None:
|
|
1134
1293
|
return f"Would replace the logical cable with {count} physical segment(s)."
|
|
1135
1294
|
return f"Would create {count} physical segment(s)."
|
|
1136
1295
|
|
|
@@ -1269,8 +1428,14 @@ def _cable_terminations(cable_id: int) -> list:
|
|
|
1269
1428
|
|
|
1270
1429
|
|
|
1271
1430
|
__all__ = (
|
|
1431
|
+
"AUTOMATICALLY_RESOLVED",
|
|
1272
1432
|
"CABLE_STATUS",
|
|
1433
|
+
"CONFLICT_SEGMENT",
|
|
1434
|
+
"CREATE_SEGMENT",
|
|
1273
1435
|
"ELIGIBLE_TERMINATION_LIMIT",
|
|
1436
|
+
"MANUALLY_RESOLVED",
|
|
1437
|
+
"REUSE_SEGMENT",
|
|
1438
|
+
"UNRESOLVED",
|
|
1274
1439
|
"CableModule",
|
|
1275
1440
|
"EligibleTerminations",
|
|
1276
1441
|
"eligible_terminations",
|
|
@@ -70,14 +70,19 @@ def termination_field_key(*, device, cards, port, kind: str, role: str = TERMINA
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _validate_termination_field_data(data):
|
|
74
|
+
"""Reject malformed termination field-key data before canonicalization."""
|
|
75
|
+
if not isinstance(data, dict) or set(data) != {"cards", "device", "kind", "port", "role"}:
|
|
76
|
+
raise ValueError
|
|
77
|
+
if not all(isinstance(data[name], str) for name in data):
|
|
78
|
+
raise ValueError
|
|
79
|
+
|
|
80
|
+
|
|
73
81
|
def parse_termination_field_key(value: str) -> dict[str, str]:
|
|
74
82
|
"""Parse an exact canonical termination field key."""
|
|
75
83
|
try:
|
|
76
84
|
data = json.loads(value)
|
|
77
|
-
|
|
78
|
-
raise ValueError
|
|
79
|
-
if not all(isinstance(data[name], str) for name in data):
|
|
80
|
-
raise ValueError
|
|
85
|
+
_validate_termination_field_data(data)
|
|
81
86
|
canonical = termination_field_key(**data)
|
|
82
87
|
except (TypeError, ValueError) as exc:
|
|
83
88
|
raise ValueError(f"'{value}' is not a canonical termination field key.") from exc
|
|
@@ -12,6 +12,7 @@ from .catalog import CATALOG
|
|
|
12
12
|
from .models import (
|
|
13
13
|
CableClassMapping,
|
|
14
14
|
ImportProfile,
|
|
15
|
+
InferenceBackend,
|
|
15
16
|
ColumnMapping,
|
|
16
17
|
ClassRoleMapping,
|
|
17
18
|
DeviceTypeMapping,
|
|
@@ -285,6 +286,32 @@ class ColumnTransformRuleForm(forms.ModelForm):
|
|
|
285
286
|
self.fields[name].choices = [("", "---------"), *preserved]
|
|
286
287
|
|
|
287
288
|
|
|
289
|
+
class InferenceBackendForm(NetBoxModelForm):
|
|
290
|
+
"""Create or edit one AI backend. Model validation applies the api_root trust boundary."""
|
|
291
|
+
|
|
292
|
+
class Meta:
|
|
293
|
+
model = InferenceBackend
|
|
294
|
+
fields = (
|
|
295
|
+
"backend_key",
|
|
296
|
+
"display_name",
|
|
297
|
+
"adapter_type",
|
|
298
|
+
"api_root",
|
|
299
|
+
"model",
|
|
300
|
+
"authentication",
|
|
301
|
+
"response_mode",
|
|
302
|
+
"credential_reference",
|
|
303
|
+
"connect_timeout",
|
|
304
|
+
"read_timeout",
|
|
305
|
+
"enabled",
|
|
306
|
+
"tags",
|
|
307
|
+
)
|
|
308
|
+
help_texts = {
|
|
309
|
+
"credential_reference": (
|
|
310
|
+
"A typed Vault KV v2 reference: backend, mount, path and field. It never holds a key value."
|
|
311
|
+
),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
288
315
|
class ImportSetupForm(forms.Form):
|
|
289
316
|
"""Form for the import wizard step 1: select profile, upload file, choose site/location/tenant."""
|
|
290
317
|
|
|
@@ -9,6 +9,7 @@ workbook, no column and no NetBox object type.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from contextlib import suppress
|
|
12
13
|
from typing import cast
|
|
13
14
|
|
|
14
15
|
from django.core.exceptions import ValidationError
|
|
@@ -201,7 +202,7 @@ class ImportEngine:
|
|
|
201
202
|
rolled_back=failure.rolled_back,
|
|
202
203
|
not_attempted=failure.not_attempted,
|
|
203
204
|
)
|
|
204
|
-
raise failure.cause
|
|
205
|
+
raise failure.cause from failure
|
|
205
206
|
except Exception as exc:
|
|
206
207
|
# Every other failure after the reservation, so the row can never stay pending.
|
|
207
208
|
cls._mark_failed(
|
|
@@ -335,10 +336,8 @@ class ImportEngine:
|
|
|
335
336
|
@staticmethod
|
|
336
337
|
def _mark_failed(execution, **detail) -> None:
|
|
337
338
|
"""Finish a failed row unless a concurrent finisher already chose its outcome."""
|
|
338
|
-
|
|
339
|
+
with suppress(ValueError):
|
|
339
340
|
execution.mark_failed(**detail)
|
|
340
|
-
except ValueError:
|
|
341
|
-
pass
|
|
342
341
|
|
|
343
342
|
@staticmethod
|
|
344
343
|
def _stored_source(profile, source_document) -> SourceDocument:
|