nautobot 2.0.0a2__py3-none-any.whl → 2.0.0b1__py3-none-any.whl
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.
- nautobot/__init__.py +1 -5
- nautobot/apps/api.py +6 -8
- nautobot/apps/forms.py +0 -2
- nautobot/apps/ui.py +0 -8
- nautobot/circuits/api/serializers.py +9 -119
- nautobot/circuits/api/urls.py +1 -1
- nautobot/circuits/api/views.py +0 -1
- nautobot/circuits/choices.py +0 -2
- nautobot/circuits/filters.py +7 -6
- nautobot/circuits/forms.py +3 -73
- nautobot/circuits/migrations/0001_initial_part_1.py +0 -1
- nautobot/circuits/migrations/0002_initial_part_2.py +0 -1
- nautobot/circuits/migrations/0003_auto_slug.py +0 -1
- nautobot/circuits/migrations/0004_increase_provider_account_length.py +0 -1
- nautobot/circuits/migrations/0005_providernetwork.py +0 -1
- nautobot/circuits/migrations/0006_cache_circuit_terminations.py +0 -1
- nautobot/circuits/migrations/0007_circuitterminations_primary_model.py +0 -1
- nautobot/circuits/migrations/0008_add_natural_indexing.py +0 -1
- nautobot/circuits/migrations/0009_circuittermination_location.py +0 -1
- nautobot/circuits/migrations/0010_rename_foreign_keys_and_related_names.py +0 -1
- nautobot/circuits/migrations/0011_remove_site_foreign_key_from_circuit_termination_class.py +0 -1
- nautobot/circuits/migrations/0012_created_datetime.py +0 -1
- nautobot/circuits/migrations/0013_alter_circuittermination__path.py +0 -1
- nautobot/circuits/migrations/0014_related_name_changes.py +1 -2
- nautobot/circuits/migrations/0015_remove_circuittype_provider_slug.py +20 -0
- nautobot/circuits/migrations/0016_tagsfield.py +34 -0
- nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
- nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
- nautobot/circuits/models.py +3 -93
- nautobot/circuits/navigation.py +14 -69
- nautobot/circuits/signals.py +0 -2
- nautobot/circuits/tables.py +42 -5
- nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -1
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -1
- nautobot/circuits/templates/circuits/provider_create.html +0 -1
- nautobot/circuits/templates/circuits/provider_retrieve.html +1 -1
- nautobot/circuits/tests/integration/test_relationships.py +13 -16
- nautobot/circuits/tests/test_api.py +13 -43
- nautobot/circuits/tests/test_filters.py +20 -15
- nautobot/circuits/tests/test_models.py +7 -3
- nautobot/circuits/tests/test_views.py +57 -67
- nautobot/circuits/views.py +18 -9
- nautobot/core/api/__init__.py +8 -2
- nautobot/core/api/authentication.py +0 -3
- nautobot/core/api/fields.py +15 -6
- nautobot/core/api/filter_backends.py +3 -2
- nautobot/core/api/metadata.py +237 -30
- nautobot/core/api/mixins.py +94 -0
- nautobot/core/api/pagination.py +3 -3
- nautobot/core/api/parsers.py +154 -0
- nautobot/core/api/renderers.py +153 -2
- nautobot/core/api/schema.py +47 -3
- nautobot/core/api/serializers.py +377 -37
- nautobot/core/api/urls.py +11 -3
- nautobot/core/api/utils.py +174 -2
- nautobot/core/api/versioning.py +32 -10
- nautobot/core/api/views.py +266 -75
- nautobot/core/apps/__init__.py +138 -221
- nautobot/core/celery/__init__.py +112 -41
- nautobot/core/celery/backends.py +19 -13
- nautobot/core/celery/control.py +46 -0
- nautobot/core/celery/encoders.py +53 -0
- nautobot/core/celery/log.py +38 -0
- nautobot/core/celery/schedulers.py +23 -4
- nautobot/core/celery/task.py +1 -16
- nautobot/core/checks.py +0 -27
- nautobot/core/choices.py +21 -113
- nautobot/core/{cli.py → cli/__init__.py} +1 -2
- nautobot/core/cli/__main__.py +3 -0
- nautobot/core/constants.py +25 -43
- nautobot/core/context_processors.py +12 -0
- nautobot/core/filters.py +2 -2
- nautobot/core/forms/__init__.py +0 -4
- nautobot/core/forms/fields.py +39 -68
- nautobot/core/forms/forms.py +27 -27
- nautobot/core/forms/utils.py +7 -59
- nautobot/core/forms/widgets.py +0 -1
- nautobot/core/graphql/__init__.py +2 -2
- nautobot/core/graphql/schema.py +4 -27
- nautobot/core/jobs/__init__.py +75 -0
- nautobot/core/management/commands/build_ui.py +255 -0
- nautobot/core/management/commands/celery.py +0 -1
- nautobot/core/management/commands/generate_test_data.py +18 -13
- nautobot/core/management/commands/post_upgrade.py +24 -24
- nautobot/core/management/commands/validate_models.py +0 -1
- nautobot/core/middleware.py +0 -1
- nautobot/core/models/__init__.py +26 -1
- nautobot/core/models/fields.py +24 -5
- nautobot/core/models/generics.py +2 -46
- nautobot/core/models/managers.py +5 -0
- nautobot/core/models/name_color_content_types.py +1 -19
- nautobot/core/models/tree_queries.py +14 -4
- nautobot/core/models/utils.py +9 -10
- nautobot/core/models/validators.py +17 -8
- nautobot/core/releases.py +8 -10
- nautobot/core/settings.py +81 -53
- nautobot/core/tables.py +5 -5
- nautobot/core/tasks.py +4 -7
- nautobot/core/templates/base.html +1 -49
- nautobot/core/templates/base_django.html +49 -0
- nautobot/core/templates/base_react.html +55 -0
- nautobot/core/templates/buttons/export.html +6 -4
- nautobot/core/templates/generic/object_bulk_create.html +10 -21
- nautobot/core/templates/generic/object_list.html +4 -1
- nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
- nautobot/core/templates/inc/footer.html +1 -0
- nautobot/core/templates/inc/javascript.html +0 -14
- nautobot/core/templates/inc/nav_menu.html +28 -33
- nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
- nautobot/core/templates/inc/relationships_table_rows.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +8 -25
- nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
- nautobot/core/templates/rest_framework/api.html +8 -0
- nautobot/core/templatetags/buttons.py +32 -29
- nautobot/core/templatetags/helpers.py +1 -1
- nautobot/core/testing/__init__.py +47 -44
- nautobot/core/testing/api.py +365 -47
- nautobot/core/testing/filters.py +12 -7
- nautobot/core/testing/integration.py +1 -1
- nautobot/core/testing/migrations.py +2 -0
- nautobot/core/testing/mixins.py +22 -12
- nautobot/core/testing/schema.py +2 -1
- nautobot/core/testing/views.py +28 -51
- nautobot/core/tests/integration/test_filters.py +17 -8
- nautobot/core/tests/integration/test_navbar.py +11 -34
- nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
- nautobot/core/tests/nautobot_config.py +2 -3
- nautobot/core/tests/runner.py +0 -1
- nautobot/core/tests/test_api.py +290 -24
- nautobot/core/tests/test_authentication.py +57 -14
- nautobot/core/tests/test_checks.py +0 -7
- nautobot/core/tests/test_choices.py +0 -1
- nautobot/core/tests/test_filters.py +117 -110
- nautobot/core/tests/test_forms.py +47 -110
- nautobot/core/tests/test_graphql.py +158 -135
- nautobot/core/tests/test_logging.py +4 -1
- nautobot/core/tests/test_managers.py +3 -5
- nautobot/core/tests/test_models.py +2 -0
- nautobot/core/tests/test_ordering.py +0 -2
- nautobot/core/tests/test_paginator.py +3 -1
- nautobot/core/tests/test_releases.py +12 -12
- nautobot/core/tests/test_templatetags_helpers.py +7 -4
- nautobot/core/tests/test_utils.py +112 -78
- nautobot/core/tests/test_views.py +12 -17
- nautobot/core/tests/test_views_utils.py +6 -9
- nautobot/core/utils/data.py +17 -0
- nautobot/core/utils/deprecation.py +13 -20
- nautobot/core/utils/filtering.py +53 -9
- nautobot/core/utils/git.py +12 -4
- nautobot/core/utils/lookup.py +3 -1
- nautobot/core/utils/requests.py +23 -116
- nautobot/core/views/__init__.py +1 -2
- nautobot/core/views/generic.py +131 -119
- nautobot/core/views/mixins.py +53 -62
- nautobot/core/views/paginator.py +0 -1
- nautobot/core/views/renderers.py +14 -12
- nautobot/core/views/utils.py +87 -4
- nautobot/dcim/api/serializers.py +160 -672
- nautobot/dcim/api/urls.py +1 -1
- nautobot/dcim/api/views.py +7 -46
- nautobot/dcim/choices.py +2 -25
- nautobot/dcim/elevations.py +0 -1
- nautobot/dcim/factory.py +15 -4
- nautobot/dcim/filters/__init__.py +42 -13
- nautobot/dcim/form_mixins.py +1 -27
- nautobot/dcim/forms.py +58 -797
- nautobot/dcim/management/commands/trace_paths.py +0 -1
- nautobot/dcim/migrations/0001_initial_part_1.py +0 -1
- nautobot/dcim/migrations/0002_initial_part_2.py +0 -1
- nautobot/dcim/migrations/0003_initial_part_3.py +0 -1
- nautobot/dcim/migrations/0004_initial_part_4.py +0 -1
- nautobot/dcim/migrations/0005_device_local_context_schema.py +0 -1
- nautobot/dcim/migrations/0006_auto_slug.py +0 -1
- nautobot/dcim/migrations/0007_device_secrets_group.py +0 -1
- nautobot/dcim/migrations/0008_increase_all_serial_lengths.py +0 -1
- nautobot/dcim/migrations/0009_add_natural_indexing.py +0 -1
- nautobot/dcim/migrations/0010_interface_status.py +0 -1
- nautobot/dcim/migrations/0011_interface_status_data_migration.py +0 -1
- nautobot/dcim/migrations/0012_interface_parent_bridge.py +0 -1
- nautobot/dcim/migrations/0013_location_location_type.py +0 -1
- nautobot/dcim/migrations/0014_location_status_data_migration.py +0 -1
- nautobot/dcim/migrations/0015_device_components__changeloggedmodel.py +0 -1
- nautobot/dcim/migrations/0016_device_components__timestamp_data_migration.py +0 -1
- nautobot/dcim/migrations/0017_locationtype_nestable.py +0 -1
- nautobot/dcim/migrations/0018_device_redundancy_group.py +0 -1
- nautobot/dcim/migrations/0019_device_redundancy_group_data_migration.py +0 -1
- nautobot/dcim/migrations/0020_move_site_fields_to_location_model.py +0 -1
- nautobot/dcim/migrations/0021_mptt_to_tree_queries.py +0 -1
- nautobot/dcim/migrations/0022_interface_mac_address_data_migration.py +0 -1
- nautobot/dcim/migrations/0023_alter_interface_mac_address.py +0 -1
- nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -2
- nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -14
- nautobot/dcim/migrations/0026_rename_device_and_rack_role.py +0 -1
- nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -2
- nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -2
- nautobot/dcim/migrations/0029_add_tree_managers_and_foreign_keys_pre_data_migration.py +0 -1
- nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
- nautobot/dcim/migrations/0031_rename_path_end_point_related_name.py +0 -1
- nautobot/dcim/migrations/0032_remove_site_foreign_key_from_dcim_models.py +0 -1
- nautobot/dcim/migrations/0033_created_datetime.py +0 -1
- nautobot/dcim/migrations/0034_fixup_fks_and_related_names.py +0 -1
- nautobot/dcim/migrations/0035_related_name_changes.py +1 -2
- nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -2
- nautobot/dcim/migrations/0037_interface_ip_addresses_m2m.py +0 -1
- nautobot/dcim/migrations/0038_alter_location_managers.py +0 -1
- nautobot/dcim/migrations/0039_remove_slug.py +24 -0
- nautobot/dcim/migrations/0040_tagsfield.py +109 -0
- nautobot/dcim/migrations/0041_ipam__namespaces.py +25 -0
- nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
- nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
- nautobot/dcim/models/cables.py +4 -35
- nautobot/dcim/models/device_component_templates.py +7 -2
- nautobot/dcim/models/device_components.py +26 -203
- nautobot/dcim/models/devices.py +30 -152
- nautobot/dcim/models/locations.py +3 -64
- nautobot/dcim/models/power.py +3 -51
- nautobot/dcim/models/racks.py +7 -86
- nautobot/dcim/navigation.py +141 -467
- nautobot/dcim/signals.py +0 -2
- nautobot/dcim/tables/devices.py +8 -5
- nautobot/dcim/tables/devicetypes.py +1 -1
- nautobot/dcim/tables/locations.py +2 -2
- nautobot/dcim/tables/power.py +2 -2
- nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/device.html +15 -4
- nautobot/dcim/templates/dcim/device_edit.html +6 -0
- nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +0 -1
- nautobot/dcim/templates/dcim/devicetype.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +4 -0
- nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/interface_edit.html +1 -0
- nautobot/dcim/templates/dcim/location.html +16 -1
- nautobot/dcim/templates/dcim/locationtype.html +15 -0
- nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/rackgroup.html +0 -12
- nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
- nautobot/dcim/tests/test_api.py +202 -130
- nautobot/dcim/tests/test_cablepaths.py +47 -42
- nautobot/dcim/tests/test_filters.py +156 -134
- nautobot/dcim/tests/test_forms.py +12 -213
- nautobot/dcim/tests/test_graphql.py +8 -3
- nautobot/dcim/tests/test_migrations.py +6 -11
- nautobot/dcim/tests/test_models.py +208 -158
- nautobot/dcim/tests/test_natural_ordering.py +12 -14
- nautobot/dcim/tests/test_signals.py +7 -4
- nautobot/dcim/tests/test_views.py +270 -264
- nautobot/dcim/urls.py +21 -26
- nautobot/dcim/views.py +14 -156
- nautobot/docs/additional-features/caching.md +6 -87
- nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
- nautobot/docs/additional-features/jobs.md +179 -197
- nautobot/docs/administration/nautobot-server.md +9 -24
- nautobot/docs/administration/nautobot-shell.md +6 -6
- nautobot/docs/administration/replicating-nautobot.md +0 -10
- nautobot/docs/configuration/index.md +9 -9
- nautobot/docs/configuration/optional-settings.md +32 -61
- nautobot/docs/configuration/required-settings.md +11 -52
- nautobot/docs/development/application-registry.md +2 -13
- nautobot/docs/development/best-practices.md +2 -1
- nautobot/docs/development/docker-compose-advanced-use-cases.md +1 -1
- nautobot/docs/development/extending-models.md +15 -17
- nautobot/docs/development/generic-views.md +0 -2
- nautobot/docs/development/getting-started.md +56 -6
- nautobot/docs/development/navigation-menu.md +22 -93
- nautobot/docs/development/react-ui.md +105 -0
- nautobot/docs/development/release-checklist.md +3 -3
- nautobot/docs/development/role-internals.md +1 -3
- nautobot/docs/development/style-guide.md +6 -4
- nautobot/docs/development/templates.md +2 -1
- nautobot/docs/docker/index.md +16 -14
- nautobot/docs/index.md +7 -3
- nautobot/docs/installation/index.md +4 -1
- nautobot/docs/installation/migrating-from-netbox.md +12 -43
- nautobot/docs/installation/migrating-from-postgresql.md +1 -1
- nautobot/docs/installation/nautobot.md +1 -1
- nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
- nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
- nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
- nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
- nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
- nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
- nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
- nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
- nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
- nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
- nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
- nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
- nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
- nautobot/docs/installation/upgrading-from-nautobot-v1.md +190 -636
- nautobot/docs/installation/upgrading.md +5 -2
- nautobot/docs/models/dcim/device.md +3 -0
- nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
- nautobot/docs/models/extras/computedfield.md +4 -4
- nautobot/docs/models/extras/dynamicgroup.md +9 -9
- nautobot/docs/models/extras/gitrepository.md +3 -0
- nautobot/docs/models/extras/job.md +1 -0
- nautobot/docs/models/extras/jobbutton.md +18 -13
- nautobot/docs/models/extras/jobhook.md +7 -4
- nautobot/docs/models/extras/jobresult.md +6 -2
- nautobot/docs/models/extras/relationship.md +2 -2
- nautobot/docs/models/extras/status.md +6 -19
- nautobot/docs/models/ipam/ipaddress.md +3 -0
- nautobot/docs/models/ipam/vrf.md +0 -3
- nautobot/docs/models/virtualization/virtualmachine.md +3 -0
- nautobot/docs/plugins/development.md +92 -24
- nautobot/docs/release-notes/version-1.5.md +96 -0
- nautobot/docs/release-notes/version-2.0.md +216 -0
- nautobot/docs/requirements.txt +5 -4
- nautobot/docs/rest-api/overview.md +384 -215
- nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
- nautobot/extras/admin.py +3 -5
- nautobot/extras/api/customfields.py +15 -39
- nautobot/extras/api/fields.py +0 -11
- nautobot/extras/api/mixins.py +45 -0
- nautobot/extras/api/relationships.py +63 -159
- nautobot/extras/api/serializers.py +165 -706
- nautobot/extras/api/urls.py +1 -1
- nautobot/extras/api/views.py +295 -282
- nautobot/extras/apps.py +4 -7
- nautobot/extras/choices.py +11 -22
- nautobot/extras/constants.py +9 -3
- nautobot/extras/datasources/__init__.py +2 -0
- nautobot/extras/datasources/git.py +135 -186
- nautobot/extras/datasources/registry.py +25 -35
- nautobot/extras/factory.py +1 -3
- nautobot/extras/filters/__init__.py +49 -47
- nautobot/extras/filters/mixins.py +10 -8
- nautobot/extras/forms/forms.py +72 -148
- nautobot/extras/forms/mixins.py +34 -57
- nautobot/extras/health_checks.py +0 -33
- nautobot/extras/jobs.py +387 -566
- nautobot/extras/management/__init__.py +55 -48
- nautobot/extras/management/commands/renaturalize.py +0 -1
- nautobot/extras/management/commands/runjob.py +24 -62
- nautobot/extras/management/commands/webhook_receiver.py +0 -1
- nautobot/extras/managers.py +30 -7
- nautobot/extras/migrations/0001_initial_part_1.py +0 -1
- nautobot/extras/migrations/0002_initial_part_2.py +0 -1
- nautobot/extras/migrations/0003_initial_part_3.py +0 -1
- nautobot/extras/migrations/0004_populate_default_status_records.py +0 -1
- nautobot/extras/migrations/0005_configcontext_device_types.py +0 -1
- nautobot/extras/migrations/0006_graphqlquery.py +0 -1
- nautobot/extras/migrations/0007_configcontextschema.py +0 -1
- nautobot/extras/migrations/0008_jobresult__custom_field_data.py +0 -1
- nautobot/extras/migrations/0009_computedfield.py +0 -1
- nautobot/extras/migrations/0010_change_cf_validation_max_min_field_to_bigint.py +0 -1
- nautobot/extras/migrations/0011_fileattachment_fileproxy.py +0 -1
- nautobot/extras/migrations/0012_healthchecktestmodel.py +0 -1
- nautobot/extras/migrations/0013_default_fallback_value_computedfield.py +0 -1
- nautobot/extras/migrations/0014_auto_slug.py +0 -1
- nautobot/extras/migrations/0015_scheduled_job.py +0 -1
- nautobot/extras/migrations/0016_secret.py +0 -1
- nautobot/extras/migrations/0017_joblogentry.py +0 -1
- nautobot/extras/migrations/0018_joblog_data_migration.py +0 -2
- nautobot/extras/migrations/0019_joblogentry__meta_options__related_name.py +0 -1
- nautobot/extras/migrations/0020_customfield_changelog.py +0 -1
- nautobot/extras/migrations/0021_customfield_changelog_data.py +0 -1
- nautobot/extras/migrations/0022_objectchange_object_datav2.py +0 -1
- nautobot/extras/migrations/0023_job_model.py +0 -1
- nautobot/extras/migrations/0024_job_data_migration.py +0 -1
- nautobot/extras/migrations/0025_add_advanced_ui_boolean_to_customfield_conputedfield_and_relationship.py +0 -1
- nautobot/extras/migrations/0026_job_add_gitrepository_fk.py +0 -1
- nautobot/extras/migrations/0027_job_gitrepository_data_migration.py +0 -1
- nautobot/extras/migrations/0028_job_reduce_source.py +0 -1
- nautobot/extras/migrations/0029_dynamicgroup.py +0 -1
- nautobot/extras/migrations/0030_webhook_alter_unique_together.py +0 -1
- nautobot/extras/migrations/0031_tag_content_types.py +0 -1
- nautobot/extras/migrations/0032_tag_content_types_data_migration.py +0 -1
- nautobot/extras/migrations/0033_add__optimized_indexing.py +0 -1
- nautobot/extras/migrations/0034_alter_fileattachment_mimetype.py +0 -1
- nautobot/extras/migrations/0035_scheduledjob_crontab.py +0 -1
- nautobot/extras/migrations/0036_job_add_has_sensitive_variables.py +0 -1
- nautobot/extras/migrations/0037_configcontextschema__remove_name_unique__create_constraint_unique_name_owner.py +0 -1
- nautobot/extras/migrations/0038_configcontext_locations.py +0 -1
- nautobot/extras/migrations/0039_objectchange__add_change_context.py +0 -1
- nautobot/extras/migrations/0040_dynamicgroup__dynamicgroupmembership.py +0 -1
- nautobot/extras/migrations/0041_jobresult_job_kwargs.py +0 -1
- nautobot/extras/migrations/0042_job__add_is_job_hook_receiver.py +0 -1
- nautobot/extras/migrations/0043_note.py +0 -1
- nautobot/extras/migrations/0044_add_job_hook.py +0 -1
- nautobot/extras/migrations/0045_add_custom_field_slug.py +0 -1
- nautobot/extras/migrations/0046_populate_custom_field_slug_label.py +0 -1
- nautobot/extras/migrations/0047_enforce_custom_field_slug.py +0 -1
- nautobot/extras/migrations/0048_alter_objectchange_change_context_detail.py +0 -1
- nautobot/extras/migrations/0049_alter_tag_slug.py +0 -1
- nautobot/extras/migrations/0050_customfield_grouping.py +0 -1
- nautobot/extras/migrations/0051_add_job_task_queues.py +0 -1
- nautobot/extras/migrations/0052_configcontext_device_redundancy_groups.py +0 -1
- nautobot/extras/migrations/0053_relationship_required_on.py +0 -1
- nautobot/extras/migrations/0054_scheduledjob_kwargs_request_user_change.py +0 -1
- nautobot/extras/migrations/0055_configcontext_dynamic_groups.py +0 -1
- nautobot/extras/migrations/0056_objectchange_add_reverse_time_idx.py +0 -1
- nautobot/extras/migrations/0057_jobbutton.py +0 -1
- nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
- nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -2
- nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -2
- nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -8
- nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -33
- nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -2
- nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -2
- nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
- nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -2
- nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -3
- nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -2
- nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -2
- nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
- nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -2
- nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
- nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -10
- nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -2
- nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
- nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
- nautobot/extras/migrations/{0076_remove_name_field_and_make_label_field_non_nullable.py → 0077_remove_name_field_and_make_label_field_non_nullable.py} +1 -1
- nautobot/extras/migrations/0078_remove_slug.py +45 -0
- nautobot/extras/migrations/0079_tagsfield.py +28 -0
- nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
- nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
- nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
- nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
- nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
- nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
- nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
- nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
- nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
- nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
- nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
- nautobot/extras/models/__init__.py +2 -3
- nautobot/extras/models/change_logging.py +0 -36
- nautobot/extras/models/customfields.py +39 -33
- nautobot/extras/models/datasources.py +48 -50
- nautobot/extras/models/groups.py +5 -12
- nautobot/extras/models/jobs.py +190 -323
- nautobot/extras/models/mixins.py +0 -71
- nautobot/extras/models/models.py +1 -22
- nautobot/extras/models/relationships.py +20 -21
- nautobot/extras/models/roles.py +0 -23
- nautobot/extras/models/secrets.py +2 -31
- nautobot/extras/models/statuses.py +6 -5
- nautobot/extras/models/tags.py +2 -17
- nautobot/extras/navigation.py +89 -307
- nautobot/extras/plugins/__init__.py +3 -121
- nautobot/extras/plugins/utils.py +0 -3
- nautobot/extras/plugins/validators.py +5 -4
- nautobot/extras/plugins/views.py +16 -4
- nautobot/extras/querysets.py +1 -7
- nautobot/extras/registry.py +3 -0
- nautobot/extras/signals.py +26 -60
- nautobot/extras/tables.py +42 -49
- nautobot/extras/tasks.py +0 -12
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema.html +16 -1
- nautobot/extras/templates/extras/customfield.html +0 -13
- nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -1
- nautobot/extras/templates/extras/gitrepository.html +3 -3
- nautobot/extras/templates/extras/inc/jobresult.html +10 -0
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/job.html +35 -25
- nautobot/extras/templates/extras/job_approval_request.html +15 -30
- nautobot/extras/templates/extras/job_detail.html +13 -31
- nautobot/extras/templates/extras/job_edit.html +14 -17
- nautobot/extras/templates/extras/jobresult.html +24 -6
- nautobot/extras/templates/extras/objectchange_list.html +1 -1
- nautobot/extras/templates/extras/scheduledjob.html +2 -2
- nautobot/extras/templates/extras/secret.html +28 -0
- nautobot/extras/templates/extras/secret_edit.html +0 -1
- nautobot/extras/templates/extras/secretsgroup_edit.html +0 -1
- nautobot/extras/templatetags/custom_links.py +0 -2
- nautobot/extras/templatetags/job_buttons.py +1 -0
- nautobot/extras/templatetags/plugins.py +0 -1
- nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
- nautobot/extras/test_jobs/atomic_transaction.py +53 -0
- nautobot/extras/test_jobs/dry_run.py +29 -0
- nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
- nautobot/extras/test_jobs/duplicate_name2.py +9 -0
- nautobot/extras/test_jobs/fail.py +23 -0
- nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
- nautobot/extras/test_jobs/file_upload_pass.py +25 -0
- nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
- nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
- nautobot/extras/test_jobs/job_button_receiver.py +28 -0
- nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
- nautobot/extras/test_jobs/job_variables.py +88 -0
- nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
- nautobot/extras/test_jobs/log_redaction.py +20 -0
- nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
- nautobot/extras/test_jobs/modify_db.py +25 -0
- nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
- nautobot/extras/test_jobs/object_var_optional.py +21 -0
- nautobot/extras/test_jobs/object_var_required.py +21 -0
- nautobot/extras/test_jobs/object_vars.py +26 -0
- nautobot/extras/test_jobs/pass.py +25 -0
- nautobot/extras/test_jobs/profiling.py +32 -0
- nautobot/extras/test_jobs/read_only_job.py +15 -0
- nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_soft_time_limit_greater_than_time_limit.py → test_jobs/soft_time_limit_greater_than_time_limit.py} +5 -1
- nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
- nautobot/extras/tests/integration/__init__.py +3 -3
- nautobot/extras/tests/integration/test_computedfields.py +1 -1
- nautobot/extras/tests/integration/test_configcontextschema.py +7 -5
- nautobot/extras/tests/integration/test_customfields.py +4 -2
- nautobot/extras/tests/integration/test_dynamicgroups.py +2 -2
- nautobot/extras/tests/integration/test_jobs.py +25 -27
- nautobot/extras/tests/integration/test_notes.py +8 -4
- nautobot/extras/tests/integration/test_plugins.py +4 -4
- nautobot/extras/tests/integration/test_relationships.py +2 -2
- nautobot/extras/tests/test_api.py +371 -381
- nautobot/extras/tests/test_changelog.py +17 -16
- nautobot/extras/tests/test_context_managers.py +5 -6
- nautobot/extras/tests/test_customfields.py +112 -73
- nautobot/extras/tests/test_datasources.py +191 -117
- nautobot/extras/tests/test_dynamicgroups.py +45 -68
- nautobot/extras/tests/test_filters.py +170 -130
- nautobot/extras/tests/test_forms.py +107 -109
- nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
- nautobot/extras/tests/test_jobs.py +271 -273
- nautobot/extras/tests/test_management.py +3 -6
- nautobot/extras/tests/test_migrations.py +5 -3
- nautobot/extras/tests/test_models.py +121 -173
- nautobot/extras/tests/test_notes.py +0 -1
- nautobot/extras/tests/test_plugins.py +55 -89
- nautobot/extras/tests/test_relationships.py +174 -130
- nautobot/extras/tests/test_tags.py +6 -12
- nautobot/extras/tests/test_utils.py +31 -1
- nautobot/extras/tests/test_views.py +223 -184
- nautobot/extras/tests/test_webhooks.py +16 -15
- nautobot/extras/urls.py +69 -69
- nautobot/extras/utils.py +137 -163
- nautobot/extras/views.py +81 -153
- nautobot/ipam/api/fields.py +17 -0
- nautobot/ipam/api/serializers.py +77 -164
- nautobot/ipam/api/urls.py +4 -1
- nautobot/ipam/api/views.py +28 -19
- nautobot/ipam/apps.py +1 -0
- nautobot/ipam/choices.py +5 -12
- nautobot/ipam/constants.py +1 -0
- nautobot/ipam/factory.py +41 -30
- nautobot/ipam/filters.py +58 -25
- nautobot/ipam/forms.py +82 -211
- nautobot/ipam/graphql/types.py +0 -9
- nautobot/ipam/lookups.py +13 -8
- nautobot/ipam/management/commands/__init__.py +0 -0
- nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
- nautobot/ipam/migrations/0001_initial_part_1.py +0 -1
- nautobot/ipam/migrations/0002_initial_part_2.py +0 -1
- nautobot/ipam/migrations/0003_remove_max_length.py +0 -1
- nautobot/ipam/migrations/0004_fixup_p2p_broadcast.py +0 -1
- nautobot/ipam/migrations/0005_auto_slug.py +0 -1
- nautobot/ipam/migrations/0006_ipaddress_nat_outside_list.py +0 -1
- nautobot/ipam/migrations/0007_add_natural_indexing.py +0 -1
- nautobot/ipam/migrations/0008_prefix_vlan_vlangroup_location.py +0 -1
- nautobot/ipam/migrations/0009_alter_vlan_name.py +0 -1
- nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -2
- nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -39
- nautobot/ipam/migrations/0012_rename_ipam_roles.py +0 -1
- nautobot/ipam/migrations/0013_delete_role.py +0 -1
- nautobot/ipam/migrations/0014_rename_foreign_keys_and_related_names.py +0 -1
- nautobot/ipam/migrations/0015_prefix_add_type.py +0 -1
- nautobot/ipam/migrations/0016_prefix_type_data_migration.py +0 -3
- nautobot/ipam/migrations/0017_prefix_remove_is_pool.py +0 -1
- nautobot/ipam/migrations/0018_remove_site_foreign_key_from_ipam_models.py +0 -1
- nautobot/ipam/migrations/0019_created_datetime.py +0 -1
- nautobot/ipam/migrations/0020_related_name_changes.py +1 -2
- nautobot/ipam/migrations/0021_prefix_add_rir_and_date_allocated.py +0 -1
- nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +3 -5
- nautobot/ipam/migrations/0023_delete_aggregate.py +0 -1
- nautobot/ipam/migrations/0024_interface_to_ipaddress_m2m.py +0 -1
- nautobot/ipam/migrations/0025_interface_ipaddress_m2m_data_migration.py +0 -1
- nautobot/ipam/migrations/0026_ipaddress_remove_assigned_object.py +0 -1
- nautobot/ipam/migrations/0027_remove_rir_slug.py +16 -0
- nautobot/ipam/migrations/0028_tagsfield.py +44 -0
- nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
- nautobot/ipam/migrations/0030_ipam__namespaces.py +231 -0
- nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
- nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
- nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
- nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
- nautobot/ipam/models.py +579 -368
- nautobot/ipam/navigation.py +36 -159
- nautobot/ipam/querysets.py +117 -90
- nautobot/ipam/signals.py +89 -0
- nautobot/ipam/tables.py +86 -28
- nautobot/ipam/templates/ipam/ipaddress.html +14 -30
- nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -0
- nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
- nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
- nautobot/ipam/templates/ipam/namespace_retrieve.html +42 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
- nautobot/ipam/templates/ipam/prefix.html +27 -33
- nautobot/ipam/templates/ipam/prefix_edit.html +7 -1
- nautobot/ipam/templates/ipam/vlangroup.html +0 -13
- nautobot/ipam/templates/ipam/vrf.html +6 -4
- nautobot/ipam/templates/ipam/vrf_edit.html +20 -2
- nautobot/ipam/tests/integration/test_prefixes.py +4 -27
- nautobot/ipam/tests/test_api.py +60 -61
- nautobot/ipam/tests/test_filters.py +187 -126
- nautobot/ipam/tests/test_forms.py +12 -6
- nautobot/ipam/tests/test_graphql.py +8 -6
- nautobot/ipam/tests/test_migrations.py +8 -13
- nautobot/ipam/tests/test_models.py +426 -274
- nautobot/ipam/tests/test_ordering.py +6 -3
- nautobot/ipam/tests/test_querysets.py +340 -96
- nautobot/ipam/tests/test_views.py +100 -55
- nautobot/ipam/urls.py +28 -5
- nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
- nautobot/ipam/utils/migrations.py +713 -0
- nautobot/ipam/views.py +237 -122
- nautobot/project-static/docs/404.html +1399 -166
- nautobot/project-static/docs/additional-features/caching.html +1416 -320
- nautobot/project-static/docs/additional-features/change-logging.html +1389 -190
- nautobot/project-static/docs/additional-features/config-contexts.html +1389 -190
- nautobot/project-static/docs/additional-features/graphql.html +1389 -190
- nautobot/project-static/docs/additional-features/healthcheck.html +1389 -190
- nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1393 -190
- nautobot/project-static/docs/additional-features/jobs.html +1677 -460
- nautobot/project-static/docs/additional-features/napalm.html +1389 -190
- nautobot/project-static/docs/additional-features/prometheus-metrics.html +1389 -190
- nautobot/project-static/docs/additional-features/template-filters.html +1389 -190
- nautobot/project-static/docs/administration/celery-queues.html +1389 -190
- nautobot/project-static/docs/administration/nautobot-server.html +1553 -375
- nautobot/project-static/docs/administration/nautobot-shell.html +1395 -196
- nautobot/project-static/docs/administration/permissions.html +1389 -190
- nautobot/project-static/docs/administration/replicating-nautobot.html +1387 -207
- nautobot/project-static/docs/apps/index.html +1389 -190
- nautobot/project-static/docs/apps/nautobot-apps.html +1387 -175
- nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js +29 -0
- nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js.map +8 -0
- nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js → search.208ed371.min.js} +9 -15
- nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js.map → search.208ed371.min.js.map} +4 -4
- nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css.map +1 -0
- nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1775 -590
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1389 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3588 -1922
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1461 -262
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1401 -170
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1396 -191
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2095 -894
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2357 -1194
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2258 -940
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1389 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1400 -201
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11068 -7861
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2867 -2224
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1389 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2641 -1573
- nautobot/project-static/docs/configuration/authentication/ldap.html +1389 -190
- nautobot/project-static/docs/configuration/authentication/remote.html +1389 -190
- nautobot/project-static/docs/configuration/authentication/sso.html +1389 -190
- nautobot/project-static/docs/configuration/index.html +1398 -199
- nautobot/project-static/docs/configuration/optional-settings.html +1418 -274
- nautobot/project-static/docs/configuration/required-settings.html +1419 -287
- nautobot/project-static/docs/core-functionality/circuits.html +1446 -247
- nautobot/project-static/docs/core-functionality/device-types.html +1448 -249
- nautobot/project-static/docs/core-functionality/devices.html +1452 -249
- nautobot/project-static/docs/core-functionality/ipam.html +1452 -253
- nautobot/project-static/docs/core-functionality/power.html +1448 -249
- nautobot/project-static/docs/core-functionality/secrets.html +1448 -249
- nautobot/project-static/docs/core-functionality/services.html +1448 -249
- nautobot/project-static/docs/core-functionality/sites-and-racks.html +1448 -249
- nautobot/project-static/docs/core-functionality/tenancy.html +1448 -249
- nautobot/project-static/docs/core-functionality/virtualization.html +1452 -249
- nautobot/project-static/docs/core-functionality/vlans.html +1448 -249
- nautobot/project-static/docs/development/application-registry.html +1393 -214
- nautobot/project-static/docs/development/best-practices.html +1392 -192
- nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1390 -191
- nautobot/project-static/docs/development/extending-models.html +1443 -257
- nautobot/project-static/docs/development/generic-views.html +1403 -174
- nautobot/project-static/docs/development/getting-started.html +1568 -262
- nautobot/project-static/docs/development/homepage.html +1389 -190
- nautobot/project-static/docs/development/index.html +1389 -190
- nautobot/project-static/docs/development/model-features.html +1389 -190
- nautobot/project-static/docs/development/natural-keys.html +1389 -190
- nautobot/project-static/docs/development/navigation-menu.html +1451 -330
- nautobot/project-static/docs/development/react-ui.html +4199 -0
- nautobot/project-static/docs/development/release-checklist.html +1392 -193
- nautobot/project-static/docs/development/role-internals.html +1402 -172
- nautobot/project-static/docs/development/style-guide.html +1399 -199
- nautobot/project-static/docs/development/templates.html +1391 -191
- nautobot/project-static/docs/development/testing.html +1389 -190
- nautobot/project-static/docs/development/user-preferences.html +1389 -190
- nautobot/project-static/docs/docker/index.html +1408 -206
- nautobot/project-static/docs/index.html +1397 -180
- nautobot/project-static/docs/installation/centos.html +1401 -170
- nautobot/project-static/docs/installation/external-authentication.html +1389 -190
- nautobot/project-static/docs/installation/http-server.html +1389 -190
- nautobot/project-static/docs/installation/index.html +1394 -191
- nautobot/project-static/docs/installation/migrating-from-netbox.html +1452 -305
- nautobot/project-static/docs/installation/migrating-from-postgresql.html +1390 -191
- nautobot/project-static/docs/installation/nautobot.html +1390 -191
- nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1389 -190
- nautobot/project-static/docs/installation/selinux-troubleshooting.html +1401 -170
- nautobot/project-static/docs/installation/services.html +1389 -190
- nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
- nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
- nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
- nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
- nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
- nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
- nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
- nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
- nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
- nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
- nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
- nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
- nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
- nautobot/project-static/docs/installation/ubuntu.html +1401 -170
- nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +4254 -1923
- nautobot/project-static/docs/installation/upgrading.html +1395 -192
- nautobot/project-static/docs/models/circuits/circuit.html +1427 -174
- nautobot/project-static/docs/models/circuits/circuittermination.html +1427 -174
- nautobot/project-static/docs/models/circuits/circuittype.html +1427 -174
- nautobot/project-static/docs/models/circuits/provider.html +1427 -174
- nautobot/project-static/docs/models/circuits/providernetwork.html +1427 -174
- nautobot/project-static/docs/models/dcim/cable.html +1458 -174
- nautobot/project-static/docs/models/dcim/consoleport.html +1427 -174
- nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/consoleserverport.html +1427 -174
- nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/device.html +1431 -174
- nautobot/project-static/docs/models/dcim/devicebay.html +1427 -174
- nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1522 -177
- nautobot/project-static/docs/models/dcim/devicetype.html +1427 -174
- nautobot/project-static/docs/models/dcim/frontport.html +1427 -174
- nautobot/project-static/docs/models/dcim/frontporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/interface.html +1427 -174
- nautobot/project-static/docs/models/dcim/interfacetemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/inventoryitem.html +1427 -174
- nautobot/project-static/docs/models/dcim/location.html +1427 -174
- nautobot/project-static/docs/models/dcim/locationtype.html +1427 -174
- nautobot/project-static/docs/models/dcim/manufacturer.html +1427 -174
- nautobot/project-static/docs/models/dcim/platform.html +1427 -174
- nautobot/project-static/docs/models/dcim/powerfeed.html +1425 -172
- nautobot/project-static/docs/models/dcim/poweroutlet.html +1427 -174
- nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/powerpanel.html +1425 -172
- nautobot/project-static/docs/models/dcim/powerport.html +1427 -174
- nautobot/project-static/docs/models/dcim/powerporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/rack.html +1427 -174
- nautobot/project-static/docs/models/dcim/rackgroup.html +1427 -174
- nautobot/project-static/docs/models/dcim/rackreservation.html +1427 -174
- nautobot/project-static/docs/models/dcim/rearport.html +1427 -174
- nautobot/project-static/docs/models/dcim/rearporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/region.html +1401 -170
- nautobot/project-static/docs/models/dcim/site.html +1401 -170
- nautobot/project-static/docs/models/dcim/virtualchassis.html +1425 -172
- nautobot/project-static/docs/models/extras/computedfield.html +1393 -194
- nautobot/project-static/docs/models/extras/configcontext.html +1465 -174
- nautobot/project-static/docs/models/extras/configcontextschema.html +1421 -168
- nautobot/project-static/docs/models/extras/customfield.html +1389 -190
- nautobot/project-static/docs/models/extras/customlink.html +1389 -190
- nautobot/project-static/docs/models/extras/dynamicgroup.html +1398 -199
- nautobot/project-static/docs/models/extras/exporttemplate.html +1389 -190
- nautobot/project-static/docs/models/extras/gitrepository.html +1393 -190
- nautobot/project-static/docs/models/extras/graphqlquery.html +1469 -171
- nautobot/project-static/docs/models/extras/imageattachment.html +1434 -181
- nautobot/project-static/docs/models/extras/job.html +1411 -157
- nautobot/project-static/docs/models/extras/jobbutton.html +1410 -207
- nautobot/project-static/docs/models/extras/jobhook.html +1397 -194
- nautobot/project-static/docs/models/extras/joblogentry.html +1408 -155
- nautobot/project-static/docs/models/extras/jobresult.html +1417 -159
- nautobot/project-static/docs/models/extras/note.html +1389 -190
- nautobot/project-static/docs/models/extras/relationship.html +1391 -192
- nautobot/project-static/docs/models/extras/role.html +1495 -198
- nautobot/project-static/docs/models/extras/secret.html +1492 -201
- nautobot/project-static/docs/models/extras/secretsgroup.html +1410 -157
- nautobot/project-static/docs/models/extras/status.html +1381 -221
- nautobot/project-static/docs/models/extras/tag.html +1389 -190
- nautobot/project-static/docs/models/extras/webhook.html +1389 -190
- nautobot/project-static/docs/models/ipam/ipaddress.html +1488 -200
- nautobot/project-static/docs/models/ipam/prefix.html +1410 -157
- nautobot/project-static/docs/models/ipam/rir.html +1410 -157
- nautobot/project-static/docs/models/ipam/routetarget.html +1410 -157
- nautobot/project-static/docs/models/ipam/service.html +1410 -157
- nautobot/project-static/docs/models/ipam/vlan.html +1410 -157
- nautobot/project-static/docs/models/ipam/vlangroup.html +1410 -157
- nautobot/project-static/docs/models/ipam/vrf.html +1410 -161
- nautobot/project-static/docs/models/tenancy/tenant.html +1412 -159
- nautobot/project-static/docs/models/tenancy/tenantgroup.html +1412 -159
- nautobot/project-static/docs/models/users/objectpermission.html +1462 -171
- nautobot/project-static/docs/models/users/token.html +1410 -157
- nautobot/project-static/docs/models/virtualization/cluster.html +1410 -157
- nautobot/project-static/docs/models/virtualization/clustergroup.html +1410 -157
- nautobot/project-static/docs/models/virtualization/clustertype.html +1410 -157
- nautobot/project-static/docs/models/virtualization/virtualmachine.html +1414 -157
- nautobot/project-static/docs/models/virtualization/vminterface.html +1410 -157
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/plugins/development.html +1916 -646
- nautobot/project-static/docs/plugins/index.html +1389 -190
- nautobot/project-static/docs/plugins/porting-from-netbox.html +1389 -190
- nautobot/project-static/docs/release-notes/index.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.0.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.1.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.2.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.3.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.4.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.5.html +2016 -397
- nautobot/project-static/docs/release-notes/version-2.0.html +1935 -287
- nautobot/project-static/docs/requirements.txt +5 -4
- nautobot/project-static/docs/rest-api/authentication.html +1389 -190
- nautobot/project-static/docs/rest-api/filtering.html +1389 -190
- nautobot/project-static/docs/rest-api/overview.html +2002 -576
- nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +197 -187
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guides/custom-fields.html +1390 -191
- nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1392 -193
- nautobot/project-static/docs/user-guides/getting-started/index.html +1388 -189
- nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1388 -189
- nautobot/project-static/docs/user-guides/getting-started/ipam.html +1386 -187
- nautobot/project-static/docs/user-guides/getting-started/platforms.html +1448 -249
- nautobot/project-static/docs/user-guides/getting-started/regions.html +1411 -212
- nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1395 -196
- nautobot/project-static/docs/user-guides/getting-started/tenants.html +1448 -249
- nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1448 -249
- nautobot/project-static/docs/user-guides/git-data-source.html +1405 -206
- nautobot/project-static/docs/user-guides/graphql.html +1402 -203
- nautobot/project-static/docs/user-guides/relationships.html +1448 -249
- nautobot/project-static/docs/user-guides/s3-django-storage.html +1448 -249
- nautobot/project-static/js/forms.js +16 -9
- nautobot/project-static/js/theme.js +5 -0
- nautobot/tenancy/api/serializers.py +4 -34
- nautobot/tenancy/api/urls.py +1 -1
- nautobot/tenancy/filters/__init__.py +9 -7
- nautobot/tenancy/filters/mixins.py +3 -2
- nautobot/tenancy/forms.py +3 -36
- nautobot/tenancy/migrations/0001_initial.py +0 -1
- nautobot/tenancy/migrations/0002_auto_slug.py +0 -1
- nautobot/tenancy/migrations/0003_mptt_to_tree_queries.py +0 -1
- nautobot/tenancy/migrations/0004_change_tree_manager_on_tree_models.py +0 -1
- nautobot/tenancy/migrations/0005_rename_foreign_keys_and_related_names.py +0 -1
- nautobot/tenancy/migrations/0006_created_datetime.py +0 -1
- nautobot/tenancy/migrations/0007_remove_tenant_tenantgroup_slug.py +20 -0
- nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
- nautobot/tenancy/models.py +0 -30
- nautobot/tenancy/navigation.py +6 -39
- nautobot/tenancy/tables.py +4 -4
- nautobot/tenancy/templates/tenancy/tenant.html +12 -12
- nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -1
- nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
- nautobot/tenancy/tests/test_api.py +1 -12
- nautobot/tenancy/tests/test_filters.py +20 -12
- nautobot/tenancy/tests/test_views.py +11 -29
- nautobot/tenancy/urls.py +10 -10
- nautobot/tenancy/views.py +0 -3
- nautobot/ui/.eslintignore +6 -0
- nautobot/ui/.gitignore +10 -0
- nautobot/ui/.prettierignore +9 -0
- nautobot/ui/.prettierrc +4 -0
- nautobot/ui/README.md +33 -0
- nautobot/ui/app_imports.js.j2 +7 -0
- nautobot/ui/craco.config.js +46 -0
- nautobot/ui/jsconfig-base.json +11 -0
- nautobot/ui/jsconfig.json +5 -0
- nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
- nautobot/ui/package-lock.json +21451 -0
- nautobot/ui/package.json +70 -0
- nautobot/ui/public/index.html +47 -0
- nautobot/ui/public/logo192.png +0 -0
- nautobot/ui/public/logo512.png +0 -0
- nautobot/ui/public/manifest.json +25 -0
- nautobot/ui/public/nautobot_logo.svg +131 -0
- nautobot/ui/public/robots.txt +3 -0
- nautobot/ui/src/App.js +71 -0
- nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
- nautobot/ui/src/components/AppTab.js +40 -0
- nautobot/ui/src/components/Apps.js +60 -0
- nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
- nautobot/ui/src/components/HomePanel.js +58 -0
- nautobot/ui/src/components/JobHistoryTable.js +78 -0
- nautobot/ui/src/components/Layout.js +53 -0
- nautobot/ui/src/components/LoadingWidget.js +25 -0
- nautobot/ui/src/components/Navbar.js +116 -0
- nautobot/ui/src/components/NotificationPopover.js +27 -0
- nautobot/ui/src/components/ObjectListTable.js +209 -0
- nautobot/ui/src/components/ReferenceDataTag.js +35 -0
- nautobot/ui/src/components/RouterButton.js +10 -0
- nautobot/ui/src/components/RouterLink.js +10 -0
- nautobot/ui/src/components/SidebarNav.js +147 -0
- nautobot/ui/src/components/Table.js +48 -0
- nautobot/ui/src/components/TableItem.js +71 -0
- nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
- nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
- nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
- nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
- nautobot/ui/src/components/__tests__/Table.test.js +36 -0
- nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
- nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
- nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
- nautobot/ui/src/components/pagination.js +93 -0
- nautobot/ui/src/components/paginator.js +79 -0
- nautobot/ui/src/components/paginator_form.js +43 -0
- nautobot/ui/src/components/usePagination.js +57 -0
- nautobot/ui/src/constants/apiPath.js +10 -0
- nautobot/ui/src/constants/icons.js +15 -0
- nautobot/ui/src/constants/size.js +15 -0
- nautobot/ui/src/index.js +65 -0
- nautobot/ui/src/reportWebVitals.js +15 -0
- nautobot/ui/src/router.js +77 -0
- nautobot/ui/src/utils/api.js +131 -0
- nautobot/ui/src/utils/app-import.js +15 -0
- nautobot/ui/src/utils/color.js +15 -0
- nautobot/ui/src/utils/date.js +14 -0
- nautobot/ui/src/utils/index.js +15 -0
- nautobot/ui/src/utils/navigation.js +32 -0
- nautobot/ui/src/utils/session.js +64 -0
- nautobot/ui/src/utils/store.js +242 -0
- nautobot/ui/src/utils/string.js +6 -0
- nautobot/ui/src/utils/url.js +4 -0
- nautobot/ui/src/views/Home.js +138 -0
- nautobot/ui/src/views/InstalledApps.js +80 -0
- nautobot/ui/src/views/Login.js +48 -0
- nautobot/ui/src/views/Logout.js +20 -0
- nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
- nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
- nautobot/ui/src/views/__tests__/Login.test.js +15 -0
- nautobot/ui/src/views/generic/GenericView.js +142 -0
- nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
- nautobot/ui/src/views/generic/ObjectList.js +127 -0
- nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
- nautobot/users/admin.py +1 -1
- nautobot/users/api/serializers.py +51 -61
- nautobot/users/api/urls.py +1 -1
- nautobot/users/api/views.py +53 -2
- nautobot/users/migrations/0001_initial.py +0 -1
- nautobot/users/migrations/0002_token_ordering_by_created.py +0 -1
- nautobot/users/migrations/0003_alter_user_options.py +0 -1
- nautobot/users/migrations/0004_alter_user_managers.py +0 -1
- nautobot/users/tests/test_api.py +109 -28
- nautobot/users/tests/test_filters.py +0 -4
- nautobot/users/tests/test_models.py +0 -1
- nautobot/users/views.py +0 -7
- nautobot/virtualization/api/serializers.py +18 -132
- nautobot/virtualization/api/urls.py +1 -1
- nautobot/virtualization/api/views.py +1 -22
- nautobot/virtualization/choices.py +0 -2
- nautobot/virtualization/filters.py +12 -7
- nautobot/virtualization/forms.py +21 -117
- nautobot/virtualization/migrations/0001_initial.py +0 -1
- nautobot/virtualization/migrations/0002_virtualmachine_local_context_schema.py +0 -1
- nautobot/virtualization/migrations/0003_vminterface_verbose_name.py +0 -1
- nautobot/virtualization/migrations/0004_auto_slug.py +0 -1
- nautobot/virtualization/migrations/0005_add_natural_indexing.py +0 -1
- nautobot/virtualization/migrations/0006_vminterface_status.py +0 -1
- nautobot/virtualization/migrations/0007_vminterface_status_data_migration.py +0 -1
- nautobot/virtualization/migrations/0008_vminterface_parent.py +0 -1
- nautobot/virtualization/migrations/0009_cluster_location.py +0 -1
- nautobot/virtualization/migrations/0010_vminterface_mac_address_data_migration.py +0 -1
- nautobot/virtualization/migrations/0011_alter_vminterface_mac_address.py +0 -1
- nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -2
- nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -12
- nautobot/virtualization/migrations/0014_rename_virtualmachine_roles.py +0 -1
- nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -2
- nautobot/virtualization/migrations/0016_remove_site_foreign_key_from_cluster_class.py +0 -1
- nautobot/virtualization/migrations/0017_created_datetime.py +0 -1
- nautobot/virtualization/migrations/0018_related_name_changes.py +1 -2
- nautobot/virtualization/migrations/0019_vminterface_ip_addresses_m2m.py +0 -1
- nautobot/virtualization/migrations/0020_remove_clustergroup_clustertype_slug.py +20 -0
- nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
- nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
- nautobot/virtualization/migrations/0023_ipam__namespaces.py +25 -0
- nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
- nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
- nautobot/virtualization/models.py +39 -131
- nautobot/virtualization/navigation.py +18 -99
- nautobot/virtualization/tables.py +4 -4
- nautobot/virtualization/templates/virtualization/virtualmachine.html +13 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
- nautobot/virtualization/tests/test_api.py +42 -52
- nautobot/virtualization/tests/test_filters.py +98 -75
- nautobot/virtualization/tests/test_models.py +36 -13
- nautobot/virtualization/tests/test_views.py +68 -73
- nautobot/virtualization/urls.py +10 -10
- nautobot/virtualization/views.py +8 -14
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +15 -22
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +987 -834
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
- nautobot/circuits/api/nested_serializers.py +0 -69
- nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
- nautobot/dcim/api/nested_serializers.py +0 -356
- nautobot/dcim/templates/dcim/device_import.html +0 -5
- nautobot/dcim/templates/dcim/device_import_child.html +0 -5
- nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
- nautobot/extras/api/nested_serializers.py +0 -353
- nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -42
- nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -47
- nautobot/extras/reports.py +0 -60
- nautobot/extras/scripts.py +0 -72
- nautobot/extras/tests/example_jobs/script_variables.py +0 -67
- nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
- nautobot/extras/tests/example_jobs/test_fail.py +0 -16
- nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
- nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
- nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
- nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
- nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
- nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
- nautobot/extras/tests/example_jobs/test_modify_db.py +0 -19
- nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
- nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
- nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
- nautobot/extras/tests/example_jobs/test_pass.py +0 -19
- nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
- nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
- nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
- nautobot/ipam/api/nested_serializers.py +0 -143
- nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js +0 -29
- nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js.map +0 -8
- nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js +0 -18
- nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js.map +0 -8
- nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css.map +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css.map +0 -1
- nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css.map +0 -1
- nautobot/tenancy/api/nested_serializers.py +0 -31
- nautobot/users/api/nested_serializers.py +0 -67
- nautobot/virtualization/api/nested_serializers.py +0 -65
- /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
- /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
nautobot/ipam/models.py
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import operator
|
|
2
3
|
|
|
3
4
|
import netaddr
|
|
4
|
-
from django.conf import settings
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
6
|
from django.core.exceptions import ValidationError, MultipleObjectsReturned
|
|
7
7
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
8
8
|
from django.db import models
|
|
9
|
-
from django.db.models import
|
|
10
|
-
from django.
|
|
11
|
-
from django.utils.functional import classproperty
|
|
9
|
+
from django.db.models import Q
|
|
10
|
+
from django.utils.functional import cached_property, classproperty
|
|
12
11
|
|
|
13
12
|
from nautobot.core.models import BaseManager, BaseModel
|
|
14
13
|
from nautobot.core.models.fields import AutoSlugField, JSONArrayField
|
|
15
14
|
from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
|
|
16
15
|
from nautobot.core.models.utils import array_to_string
|
|
17
16
|
from nautobot.core.utils.data import UtilizationData
|
|
18
|
-
from nautobot.dcim.models import
|
|
19
|
-
from nautobot.extras.models import
|
|
17
|
+
from nautobot.dcim.models import Interface
|
|
18
|
+
from nautobot.extras.models import RoleField, Status, StatusField
|
|
20
19
|
from nautobot.extras.utils import extras_features
|
|
21
20
|
from nautobot.ipam import choices
|
|
22
|
-
from nautobot.virtualization.models import
|
|
21
|
+
from nautobot.virtualization.models import VMInterface
|
|
23
22
|
from .constants import (
|
|
24
|
-
IPADDRESS_ROLES_NONUNIQUE,
|
|
25
23
|
SERVICE_PORT_MAX,
|
|
26
24
|
SERVICE_PORT_MIN,
|
|
27
25
|
VRF_RD_MAX_LENGTH,
|
|
@@ -46,6 +44,49 @@ __all__ = (
|
|
|
46
44
|
logger = logging.getLogger(__name__)
|
|
47
45
|
|
|
48
46
|
|
|
47
|
+
@extras_features(
|
|
48
|
+
"custom_links",
|
|
49
|
+
"custom_validators",
|
|
50
|
+
"dynamic_groups",
|
|
51
|
+
"export_templates",
|
|
52
|
+
"graphql",
|
|
53
|
+
"locations",
|
|
54
|
+
"webhooks",
|
|
55
|
+
)
|
|
56
|
+
class Namespace(PrimaryModel):
|
|
57
|
+
"""Container for unique IPAM objects."""
|
|
58
|
+
|
|
59
|
+
name = models.CharField(max_length=255, unique=True, db_index=True)
|
|
60
|
+
description = models.CharField(max_length=200, blank=True)
|
|
61
|
+
location = models.ForeignKey(
|
|
62
|
+
to="dcim.Location",
|
|
63
|
+
on_delete=models.PROTECT,
|
|
64
|
+
related_name="namespaces",
|
|
65
|
+
blank=True,
|
|
66
|
+
null=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def ip_addresses(self):
|
|
71
|
+
"""Return all IPAddresses associated to this Namespace through their parent Prefix."""
|
|
72
|
+
return IPAddress.objects.filter(parent__namespace=self).distinct()
|
|
73
|
+
|
|
74
|
+
class Meta:
|
|
75
|
+
ordering = ("name",)
|
|
76
|
+
|
|
77
|
+
def __str__(self):
|
|
78
|
+
return self.name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_default_namespace():
|
|
82
|
+
"""Return the Global namespace for use in default value for foreign keys."""
|
|
83
|
+
obj, _ = Namespace.objects.get_or_create(
|
|
84
|
+
name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return obj.pk
|
|
88
|
+
|
|
89
|
+
|
|
49
90
|
@extras_features(
|
|
50
91
|
"custom_links",
|
|
51
92
|
"custom_validators",
|
|
@@ -63,12 +104,34 @@ class VRF(PrimaryModel):
|
|
|
63
104
|
name = models.CharField(max_length=100, db_index=True)
|
|
64
105
|
rd = models.CharField(
|
|
65
106
|
max_length=VRF_RD_MAX_LENGTH,
|
|
66
|
-
unique=True,
|
|
67
107
|
blank=True,
|
|
68
108
|
null=True,
|
|
69
109
|
verbose_name="Route distinguisher",
|
|
70
110
|
help_text="Unique route distinguisher (as defined in RFC 4364)",
|
|
71
111
|
)
|
|
112
|
+
namespace = models.ForeignKey(
|
|
113
|
+
"ipam.Namespace",
|
|
114
|
+
on_delete=models.PROTECT,
|
|
115
|
+
related_name="vrfs",
|
|
116
|
+
default=get_default_namespace,
|
|
117
|
+
)
|
|
118
|
+
devices = models.ManyToManyField(
|
|
119
|
+
to="dcim.Device",
|
|
120
|
+
related_name="vrfs",
|
|
121
|
+
through="ipam.VRFDeviceAssignment",
|
|
122
|
+
through_fields=("vrf", "device"),
|
|
123
|
+
)
|
|
124
|
+
virtual_machines = models.ManyToManyField(
|
|
125
|
+
to="virtualization.VirtualMachine",
|
|
126
|
+
related_name="vrfs",
|
|
127
|
+
through="ipam.VRFDeviceAssignment",
|
|
128
|
+
through_fields=("vrf", "virtual_machine"),
|
|
129
|
+
)
|
|
130
|
+
prefixes = models.ManyToManyField(
|
|
131
|
+
to="ipam.Prefix",
|
|
132
|
+
related_name="vrfs",
|
|
133
|
+
through="ipam.VRFPrefixAssignment",
|
|
134
|
+
)
|
|
72
135
|
tenant = models.ForeignKey(
|
|
73
136
|
to="tenancy.Tenant",
|
|
74
137
|
on_delete=models.PROTECT,
|
|
@@ -76,48 +139,186 @@ class VRF(PrimaryModel):
|
|
|
76
139
|
blank=True,
|
|
77
140
|
null=True,
|
|
78
141
|
)
|
|
79
|
-
enforce_unique = models.BooleanField(
|
|
80
|
-
default=True,
|
|
81
|
-
verbose_name="Enforce unique space",
|
|
82
|
-
help_text="Prevent duplicate prefixes/IP addresses within this VRF",
|
|
83
|
-
)
|
|
84
142
|
description = models.CharField(max_length=200, blank=True)
|
|
85
143
|
import_targets = models.ManyToManyField(to="ipam.RouteTarget", related_name="importing_vrfs", blank=True)
|
|
86
144
|
export_targets = models.ManyToManyField(to="ipam.RouteTarget", related_name="exporting_vrfs", blank=True)
|
|
87
145
|
|
|
88
|
-
csv_headers = ["name", "rd", "tenant", "enforce_unique", "description"]
|
|
89
146
|
clone_fields = [
|
|
90
147
|
"tenant",
|
|
91
|
-
"enforce_unique",
|
|
92
148
|
"description",
|
|
93
149
|
]
|
|
94
150
|
|
|
95
151
|
class Meta:
|
|
96
|
-
ordering = ("name", "rd") # (name, rd) may be non-unique
|
|
152
|
+
ordering = ("namespace", "name", "rd") # (name, rd) may be non-unique
|
|
153
|
+
unique_together = [
|
|
154
|
+
["namespace", "name"],
|
|
155
|
+
["namespace", "rd"],
|
|
156
|
+
]
|
|
97
157
|
verbose_name = "VRF"
|
|
98
158
|
verbose_name_plural = "VRFs"
|
|
99
159
|
|
|
100
160
|
def __str__(self):
|
|
101
161
|
return self.display or super().__str__()
|
|
102
162
|
|
|
103
|
-
def get_absolute_url(self):
|
|
104
|
-
return reverse("ipam:vrf", args=[self.pk])
|
|
105
|
-
|
|
106
|
-
def to_csv(self):
|
|
107
|
-
return (
|
|
108
|
-
self.name,
|
|
109
|
-
self.rd,
|
|
110
|
-
self.tenant.name if self.tenant else None,
|
|
111
|
-
str(self.enforce_unique),
|
|
112
|
-
self.description,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
163
|
@property
|
|
116
164
|
def display(self):
|
|
117
|
-
if self.
|
|
118
|
-
return f"{self.
|
|
165
|
+
if self.namespace:
|
|
166
|
+
return f"{self.namespace}: ({self.name})"
|
|
119
167
|
return self.name
|
|
120
168
|
|
|
169
|
+
def add_device(self, device, rd="", name=""):
|
|
170
|
+
"""
|
|
171
|
+
Add a `device` to this VRF, optionally overloading `rd` and `name`.
|
|
172
|
+
|
|
173
|
+
If `rd` or `name` are not provided, the values from this VRF will be inherited.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
device (Device): Device instance
|
|
177
|
+
rd (str): (Optional) RD of the VRF when associated with this Device
|
|
178
|
+
name (str): (Optional) Name of the VRF when associated with this Device
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
VRFDeviceAssignment instance
|
|
182
|
+
"""
|
|
183
|
+
instance = self.devices.through(vrf=self, device=device, rd=rd, name=name)
|
|
184
|
+
instance.validated_save()
|
|
185
|
+
return instance
|
|
186
|
+
|
|
187
|
+
def remove_device(self, device):
|
|
188
|
+
"""
|
|
189
|
+
Remove a `device` from this VRF.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
device (Device): Device instance
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
tuple (int, dict): Number of objects deleted and a dict with number of deletions.
|
|
196
|
+
"""
|
|
197
|
+
instance = self.devices.through.objects.get(vrf=self, device=device)
|
|
198
|
+
return instance.delete()
|
|
199
|
+
|
|
200
|
+
def add_virtual_machine(self, virtual_machine, rd="", name=""):
|
|
201
|
+
"""
|
|
202
|
+
Add a `virtual_machine` to this VRF, optionally overloading `rd` and `name`.
|
|
203
|
+
|
|
204
|
+
If `rd` or `name` are not provided, the values from this VRF will be inherited.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
virtual_machine (VirtualMachine): VirtualMachine instance
|
|
208
|
+
rd (str): (Optional) RD of the VRF when associated with this VirtualMachine
|
|
209
|
+
name (str): (Optional) Name of the VRF when associated with this VirtualMachine
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
VRFDeviceAssignment instance
|
|
213
|
+
"""
|
|
214
|
+
instance = self.virtual_machines.through(vrf=self, virtual_machine=virtual_machine, rd=rd, name=name)
|
|
215
|
+
instance.validated_save()
|
|
216
|
+
return instance
|
|
217
|
+
|
|
218
|
+
def remove_virtual_machine(self, virtual_machine):
|
|
219
|
+
"""
|
|
220
|
+
Remove a `virtual_machine` from this VRF.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
virtual_machine (VirtualMachine): VirtualMachine instance
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
tuple (int, dict): Number of objects deleted and a dict with number of deletions.
|
|
227
|
+
"""
|
|
228
|
+
instance = self.virtual_machines.through.objects.get(vrf=self, virtual_machine=virtual_machine)
|
|
229
|
+
return instance.delete()
|
|
230
|
+
|
|
231
|
+
def add_prefix(self, prefix):
|
|
232
|
+
"""
|
|
233
|
+
Add a `prefix` to this VRF. Each object must be in the same Namespace.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
prefix (Prefix): Prefix instance
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
VRFPrefixAssignment instance
|
|
240
|
+
"""
|
|
241
|
+
instance = self.prefixes.through(vrf=self, prefix=prefix)
|
|
242
|
+
instance.validated_save()
|
|
243
|
+
return instance
|
|
244
|
+
|
|
245
|
+
def remove_prefix(self, prefix):
|
|
246
|
+
"""
|
|
247
|
+
Remove a `prefix` from this VRF.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
prefix (Prefix): Prefix instance
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
tuple (int, dict): Number of objects deleted and a dict with number of deletions.
|
|
254
|
+
"""
|
|
255
|
+
instance = self.prefixes.through.objects.get(vrf=self, prefix=prefix)
|
|
256
|
+
return instance.delete()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class VRFDeviceAssignment(BaseModel):
|
|
260
|
+
vrf = models.ForeignKey("ipam.VRF", on_delete=models.CASCADE, related_name="device_assignments")
|
|
261
|
+
device = models.ForeignKey(
|
|
262
|
+
"dcim.Device", null=True, blank=True, on_delete=models.CASCADE, related_name="vrf_assignments"
|
|
263
|
+
)
|
|
264
|
+
virtual_machine = models.ForeignKey(
|
|
265
|
+
"virtualization.VirtualMachine", null=True, blank=True, on_delete=models.CASCADE, related_name="vrf_assignments"
|
|
266
|
+
)
|
|
267
|
+
rd = models.CharField(
|
|
268
|
+
max_length=VRF_RD_MAX_LENGTH,
|
|
269
|
+
blank=True,
|
|
270
|
+
null=True,
|
|
271
|
+
verbose_name="Route distinguisher",
|
|
272
|
+
help_text="Unique route distinguisher (as defined in RFC 4364)",
|
|
273
|
+
)
|
|
274
|
+
name = models.CharField(blank=True, max_length=100)
|
|
275
|
+
|
|
276
|
+
class Meta:
|
|
277
|
+
unique_together = [
|
|
278
|
+
["vrf", "device"],
|
|
279
|
+
["vrf", "virtual_machine"],
|
|
280
|
+
["device", "rd", "name"],
|
|
281
|
+
["virtual_machine", "rd", "name"],
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
def __str__(self):
|
|
285
|
+
obj = self.device or self.virtual_machine
|
|
286
|
+
return f"{self.vrf} [{obj}] (rd: {self.rd}, name: {self.name})"
|
|
287
|
+
|
|
288
|
+
def clean(self):
|
|
289
|
+
super().clean()
|
|
290
|
+
|
|
291
|
+
# If RD is not set, inherit it from `vrf.rd`.
|
|
292
|
+
if not self.rd:
|
|
293
|
+
self.rd = self.vrf.rd
|
|
294
|
+
|
|
295
|
+
# If name is not set, inherit it from `vrf.name`.
|
|
296
|
+
if not self.name:
|
|
297
|
+
self.name = self.vrf.name
|
|
298
|
+
|
|
299
|
+
# A VRF must belong to a Device *or* to a VirtualMachine.
|
|
300
|
+
if all([self.device, self.virtual_machine]):
|
|
301
|
+
raise ValidationError("A VRF cannot be associated with both a device and a virtual machine.")
|
|
302
|
+
if not any([self.device, self.virtual_machine]):
|
|
303
|
+
raise ValidationError("A VRF must be associated with either a device or a virtual machine.")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class VRFPrefixAssignment(BaseModel):
|
|
307
|
+
vrf = models.ForeignKey("ipam.VRF", on_delete=models.CASCADE, related_name="+")
|
|
308
|
+
prefix = models.ForeignKey("ipam.Prefix", on_delete=models.CASCADE, related_name="vrf_assignments")
|
|
309
|
+
|
|
310
|
+
class Meta:
|
|
311
|
+
unique_together = ["vrf", "prefix"]
|
|
312
|
+
|
|
313
|
+
def __str__(self):
|
|
314
|
+
return f"{self.vrf}: {self.prefix}"
|
|
315
|
+
|
|
316
|
+
def clean(self):
|
|
317
|
+
super().clean()
|
|
318
|
+
|
|
319
|
+
if self.prefix.namespace != self.vrf.namespace:
|
|
320
|
+
raise ValidationError({"prefix": "Prefix must be in same namespace as VRF"})
|
|
321
|
+
|
|
121
322
|
|
|
122
323
|
@extras_features(
|
|
123
324
|
"custom_links",
|
|
@@ -145,24 +346,12 @@ class RouteTarget(PrimaryModel):
|
|
|
145
346
|
null=True,
|
|
146
347
|
)
|
|
147
348
|
|
|
148
|
-
csv_headers = ["name", "description", "tenant"]
|
|
149
|
-
|
|
150
349
|
class Meta:
|
|
151
350
|
ordering = ["name"]
|
|
152
351
|
|
|
153
352
|
def __str__(self):
|
|
154
353
|
return self.name
|
|
155
354
|
|
|
156
|
-
def get_absolute_url(self):
|
|
157
|
-
return reverse("ipam:routetarget", args=[self.pk])
|
|
158
|
-
|
|
159
|
-
def to_csv(self):
|
|
160
|
-
return (
|
|
161
|
-
self.name,
|
|
162
|
-
self.description,
|
|
163
|
-
self.tenant.name if self.tenant else None,
|
|
164
|
-
)
|
|
165
|
-
|
|
166
355
|
|
|
167
356
|
@extras_features(
|
|
168
357
|
"custom_validators",
|
|
@@ -175,7 +364,6 @@ class RIR(OrganizationalModel):
|
|
|
175
364
|
"""
|
|
176
365
|
|
|
177
366
|
name = models.CharField(max_length=100, unique=True)
|
|
178
|
-
slug = AutoSlugField(populate_from="name")
|
|
179
367
|
is_private = models.BooleanField(
|
|
180
368
|
default=False,
|
|
181
369
|
verbose_name="Private",
|
|
@@ -183,8 +371,6 @@ class RIR(OrganizationalModel):
|
|
|
183
371
|
)
|
|
184
372
|
description = models.CharField(max_length=200, blank=True)
|
|
185
373
|
|
|
186
|
-
csv_headers = ["name", "slug", "is_private", "description"]
|
|
187
|
-
|
|
188
374
|
objects = BaseManager.from_queryset(RIRQuerySet)()
|
|
189
375
|
|
|
190
376
|
class Meta:
|
|
@@ -195,20 +381,6 @@ class RIR(OrganizationalModel):
|
|
|
195
381
|
def __str__(self):
|
|
196
382
|
return self.name
|
|
197
383
|
|
|
198
|
-
def natural_key(self):
|
|
199
|
-
return (self.name,)
|
|
200
|
-
|
|
201
|
-
def get_absolute_url(self):
|
|
202
|
-
return reverse("ipam:rir", args=[self.slug])
|
|
203
|
-
|
|
204
|
-
def to_csv(self):
|
|
205
|
-
return (
|
|
206
|
-
self.name,
|
|
207
|
-
self.slug,
|
|
208
|
-
str(self.is_private),
|
|
209
|
-
self.description,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
384
|
|
|
213
385
|
@extras_features(
|
|
214
386
|
"custom_links",
|
|
@@ -220,12 +392,13 @@ class RIR(OrganizationalModel):
|
|
|
220
392
|
"statuses",
|
|
221
393
|
"webhooks",
|
|
222
394
|
)
|
|
223
|
-
class Prefix(PrimaryModel
|
|
395
|
+
class Prefix(PrimaryModel):
|
|
224
396
|
"""
|
|
225
397
|
A Prefix represents an IPv4 or IPv6 network, including mask length.
|
|
226
398
|
Prefixes can optionally be assigned to Locations and VRFs.
|
|
227
399
|
A Prefix must be assigned a status and may optionally be assigned a user-defined Role.
|
|
228
400
|
A Prefix can also be assigned to a VLAN where appropriate.
|
|
401
|
+
Prefixes are always ordered by `namespace` and `ip_version`, then by `network` and `prefix_length`.
|
|
229
402
|
"""
|
|
230
403
|
|
|
231
404
|
network = VarbinaryIPField(
|
|
@@ -240,6 +413,23 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
240
413
|
choices=choices.PrefixTypeChoices,
|
|
241
414
|
default=choices.PrefixTypeChoices.TYPE_NETWORK,
|
|
242
415
|
)
|
|
416
|
+
status = StatusField(blank=False, null=False)
|
|
417
|
+
role = RoleField(blank=True, null=True)
|
|
418
|
+
parent = models.ForeignKey(
|
|
419
|
+
"self",
|
|
420
|
+
blank=True,
|
|
421
|
+
null=True,
|
|
422
|
+
related_name="children", # `IPAddress` to use `related_name="ip_addresses"`
|
|
423
|
+
on_delete=models.PROTECT,
|
|
424
|
+
help_text="The parent Prefix of this Prefix.",
|
|
425
|
+
)
|
|
426
|
+
# ip_version is set internally just like network, broadcast, and prefix_length.
|
|
427
|
+
ip_version = models.IntegerField(
|
|
428
|
+
choices=choices.IPAddressVersionChoices,
|
|
429
|
+
null=True,
|
|
430
|
+
editable=False,
|
|
431
|
+
db_index=True,
|
|
432
|
+
)
|
|
243
433
|
location = models.ForeignKey(
|
|
244
434
|
to="dcim.Location",
|
|
245
435
|
on_delete=models.PROTECT,
|
|
@@ -247,13 +437,11 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
247
437
|
blank=True,
|
|
248
438
|
null=True,
|
|
249
439
|
)
|
|
250
|
-
|
|
251
|
-
to="ipam.
|
|
440
|
+
namespace = models.ForeignKey(
|
|
441
|
+
to="ipam.Namespace",
|
|
252
442
|
on_delete=models.PROTECT,
|
|
253
443
|
related_name="prefixes",
|
|
254
|
-
|
|
255
|
-
null=True,
|
|
256
|
-
verbose_name="VRF",
|
|
444
|
+
default=get_default_namespace,
|
|
257
445
|
)
|
|
258
446
|
tenant = models.ForeignKey(
|
|
259
447
|
to="tenancy.Tenant",
|
|
@@ -288,66 +476,60 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
288
476
|
|
|
289
477
|
objects = BaseManager.from_queryset(PrefixQuerySet)()
|
|
290
478
|
|
|
291
|
-
# TODO: The current Prefix model has no appropriate natural key available yet.
|
|
292
|
-
# However, by default all BaseModel subclasses now have a `natural_key` property;
|
|
293
|
-
# but for this model, accessing the natural_key will raise an exception.
|
|
294
|
-
# The below is a hacky way to "remove" the natural_key property from this model class for the time being.
|
|
295
|
-
class AttributeRemover:
|
|
296
|
-
def __get__(self, instance, owner):
|
|
297
|
-
raise AttributeError("Prefix doesn't yet have a natural key!")
|
|
298
|
-
|
|
299
|
-
natural_key = AttributeRemover()
|
|
300
|
-
|
|
301
|
-
csv_headers = [
|
|
302
|
-
"prefix",
|
|
303
|
-
"type",
|
|
304
|
-
"vrf",
|
|
305
|
-
"tenant",
|
|
306
|
-
"location",
|
|
307
|
-
"vlan_group",
|
|
308
|
-
"vlan",
|
|
309
|
-
"status",
|
|
310
|
-
"role",
|
|
311
|
-
"rir",
|
|
312
|
-
"date_allocated",
|
|
313
|
-
"description",
|
|
314
|
-
]
|
|
315
479
|
clone_fields = [
|
|
316
480
|
"date_allocated",
|
|
317
481
|
"description",
|
|
318
482
|
"location",
|
|
483
|
+
"namespace",
|
|
319
484
|
"rir",
|
|
320
485
|
"role",
|
|
321
486
|
"status",
|
|
322
487
|
"tenant",
|
|
323
488
|
"type",
|
|
324
489
|
"vlan",
|
|
325
|
-
"vrf",
|
|
326
490
|
]
|
|
491
|
+
"""
|
|
327
492
|
dynamic_group_filter_fields = {
|
|
328
493
|
"vrf": "vrf_id", # Duplicate filter fields that will be collapsed in 2.0
|
|
329
494
|
}
|
|
495
|
+
"""
|
|
330
496
|
|
|
331
497
|
class Meta:
|
|
332
498
|
ordering = (
|
|
333
|
-
|
|
499
|
+
"namespace",
|
|
500
|
+
"ip_version",
|
|
334
501
|
"network",
|
|
335
502
|
"prefix_length",
|
|
336
|
-
)
|
|
503
|
+
)
|
|
504
|
+
index_together = [
|
|
505
|
+
["network", "broadcast", "prefix_length"],
|
|
506
|
+
["namespace", "network", "broadcast", "prefix_length"],
|
|
507
|
+
]
|
|
508
|
+
unique_together = ["namespace", "network", "prefix_length"]
|
|
337
509
|
verbose_name_plural = "prefixes"
|
|
338
510
|
|
|
511
|
+
def validate_unique(self, exclude=None):
|
|
512
|
+
if self.namespace is None:
|
|
513
|
+
if Prefix.objects.filter(
|
|
514
|
+
network=self.network, prefix_length=self.prefix_length, namespace__isnull=True
|
|
515
|
+
).exists():
|
|
516
|
+
raise ValidationError(
|
|
517
|
+
{"__all__": "Prefix with this Namespace, Network and Prefix length already exists."}
|
|
518
|
+
)
|
|
519
|
+
super().validate_unique(exclude)
|
|
520
|
+
|
|
339
521
|
def __init__(self, *args, **kwargs):
|
|
340
522
|
prefix = kwargs.pop("prefix", None)
|
|
341
|
-
super(
|
|
523
|
+
super().__init__(*args, **kwargs)
|
|
342
524
|
self._deconstruct_prefix(prefix)
|
|
343
525
|
|
|
344
526
|
def __str__(self):
|
|
345
527
|
return str(self.prefix)
|
|
346
528
|
|
|
347
|
-
def _deconstruct_prefix(self,
|
|
348
|
-
if
|
|
349
|
-
if isinstance(
|
|
350
|
-
|
|
529
|
+
def _deconstruct_prefix(self, prefix):
|
|
530
|
+
if prefix:
|
|
531
|
+
if isinstance(prefix, str):
|
|
532
|
+
prefix = netaddr.IPNetwork(prefix)
|
|
351
533
|
# Note that our "broadcast" field is actually the last IP address in this prefix.
|
|
352
534
|
# This is different from the more accurate technical meaning of a network's broadcast address in 2 cases:
|
|
353
535
|
# 1. For a point-to-point prefix (IPv4 /31 or IPv6 /127), there are two addresses in the prefix,
|
|
@@ -356,62 +538,74 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
356
538
|
# We store this address as both the network and the "broadcast".
|
|
357
539
|
# This variance is intentional in both cases as we use the "broadcast" primarily for filtering and grouping
|
|
358
540
|
# of addresses and prefixes, not for packet forwarding. :-)
|
|
359
|
-
broadcast =
|
|
360
|
-
self.network = str(
|
|
541
|
+
broadcast = prefix.broadcast if prefix.broadcast else prefix[-1]
|
|
542
|
+
self.network = str(prefix.network)
|
|
361
543
|
self.broadcast = str(broadcast)
|
|
362
|
-
self.prefix_length =
|
|
544
|
+
self.prefix_length = prefix.prefixlen
|
|
545
|
+
self.ip_version = prefix.version
|
|
363
546
|
|
|
364
|
-
|
|
365
|
-
|
|
547
|
+
# TODO: this function is completely unused at present - remove?
|
|
548
|
+
def get_duplicates(self):
|
|
549
|
+
return Prefix.objects.net_equals(self.prefix).filter(namespace=self.namespace).exclude(pk=self.pk)
|
|
366
550
|
|
|
367
551
|
def clean(self):
|
|
368
552
|
super().clean()
|
|
369
553
|
|
|
370
|
-
if self.prefix:
|
|
371
|
-
|
|
372
|
-
# /0 masks are not acceptable
|
|
373
|
-
if self.prefix.prefixlen == 0:
|
|
374
|
-
raise ValidationError({"prefix": "Cannot create prefix with /0 mask."})
|
|
375
|
-
|
|
376
|
-
# Enforce unique IP space (if applicable)
|
|
377
|
-
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
|
|
378
|
-
duplicate_prefixes = self.get_duplicates()
|
|
379
|
-
if duplicate_prefixes:
|
|
380
|
-
vrf = f"VRF {self.vrf}" if self.vrf else "global table"
|
|
381
|
-
raise ValidationError({"prefix": f"Duplicate prefix found in {vrf}: {duplicate_prefixes.first()}"})
|
|
382
|
-
|
|
383
554
|
# Validate location
|
|
384
555
|
if self.location is not None:
|
|
385
|
-
|
|
386
556
|
if ContentType.objects.get_for_model(self) not in self.location.location_type.content_types.all():
|
|
387
557
|
raise ValidationError(
|
|
388
558
|
{"location": f'Prefixes may not associate to locations of type "{self.location.location_type}".'}
|
|
389
559
|
)
|
|
390
560
|
|
|
391
|
-
def
|
|
561
|
+
def delete(self, *args, **kwargs):
|
|
562
|
+
"""
|
|
563
|
+
A Prefix with children will be impossible to delete and raise a `ProtectedError`.
|
|
392
564
|
|
|
393
|
-
|
|
565
|
+
If a Prefix has children, this catch the error and explicitly update the
|
|
566
|
+
`protected_objects` from the exception setting their parent to the old parent of this
|
|
567
|
+
prefix, and then this prefix will be deleted.
|
|
568
|
+
"""
|
|
394
569
|
|
|
570
|
+
try:
|
|
571
|
+
return super().delete(*args, **kwargs)
|
|
572
|
+
except models.ProtectedError as err:
|
|
573
|
+
# This will be either IPAddress or Prefix.
|
|
574
|
+
protected_model = tuple(err.protected_objects)[0]._meta.model
|
|
575
|
+
|
|
576
|
+
# IPAddress objects must have a parent.
|
|
577
|
+
if protected_model == IPAddress and self.parent is None:
|
|
578
|
+
raise models.ProtectedError(
|
|
579
|
+
msg=(
|
|
580
|
+
f"Cannot delete Prefix {self} because it has child IPAddress objects that "
|
|
581
|
+
"would no longer have a parent."
|
|
582
|
+
),
|
|
583
|
+
protected_objects=err.protected_objects,
|
|
584
|
+
) from err
|
|
585
|
+
|
|
586
|
+
# Update protected objects to use the new parent and delete the old parent (self).
|
|
587
|
+
protected_pks = (po.pk for po in err.protected_objects)
|
|
588
|
+
protected_objects = protected_model.objects.filter(pk__in=protected_pks)
|
|
589
|
+
protected_objects.update(parent=self.parent)
|
|
590
|
+
return super().delete(*args, **kwargs)
|
|
591
|
+
|
|
592
|
+
def save(self, *args, **kwargs):
|
|
593
|
+
if isinstance(self.prefix, netaddr.IPNetwork):
|
|
395
594
|
# Clear host bits from prefix
|
|
396
595
|
self.prefix = self.prefix.cidr
|
|
397
596
|
|
|
597
|
+
# Determine if a parent exists and set it to the closest ancestor by `prefix_length`.
|
|
598
|
+
supernets = self.supernets()
|
|
599
|
+
if supernets:
|
|
600
|
+
parent = max(supernets, key=operator.attrgetter("prefix_length"))
|
|
601
|
+
self.parent = parent
|
|
602
|
+
|
|
398
603
|
super().save(*args, **kwargs)
|
|
399
604
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
self.vrf.name if self.vrf else None,
|
|
405
|
-
self.tenant.name if self.tenant else None,
|
|
406
|
-
self.location.name if self.location else None,
|
|
407
|
-
self.vlan.vlan_group.name if self.vlan and self.vlan.vlan_group else None,
|
|
408
|
-
self.vlan.vid if self.vlan else None,
|
|
409
|
-
self.get_status_display(),
|
|
410
|
-
self.role.name if self.role else None,
|
|
411
|
-
self.rir.name if self.rir else None,
|
|
412
|
-
str(self.date_allocated),
|
|
413
|
-
self.description,
|
|
414
|
-
)
|
|
605
|
+
# Determine the subnets and reparent them to this prefix.
|
|
606
|
+
self.reparent_subnets()
|
|
607
|
+
# Determine the child IPs and reparent them to this prefix.
|
|
608
|
+
self.reparent_ips()
|
|
415
609
|
|
|
416
610
|
@property
|
|
417
611
|
def cidr_str(self):
|
|
@@ -429,41 +623,168 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
429
623
|
def prefix(self, prefix):
|
|
430
624
|
self._deconstruct_prefix(prefix)
|
|
431
625
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return self.prefix.version
|
|
436
|
-
return None
|
|
626
|
+
def reparent_subnets(self):
|
|
627
|
+
"""
|
|
628
|
+
Determine the list of child Prefixes and set the parent to self.
|
|
437
629
|
|
|
438
|
-
|
|
439
|
-
|
|
630
|
+
This query is similiar performing update from the query returned by `subnets(direct=True)`,
|
|
631
|
+
but explicitly filters for subnets of the parent of this Prefix so they can be reparented.
|
|
632
|
+
"""
|
|
633
|
+
query = Prefix.objects.select_for_update().filter(
|
|
634
|
+
~models.Q(id=self.id), # Don't include yourself...
|
|
635
|
+
parent_id=self.parent_id,
|
|
636
|
+
prefix_length__gt=self.prefix_length,
|
|
637
|
+
ip_version=self.ip_version,
|
|
638
|
+
network__gte=self.network,
|
|
639
|
+
broadcast__lte=self.broadcast,
|
|
640
|
+
namespace=self.namespace,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return query.update(parent=self)
|
|
644
|
+
|
|
645
|
+
def reparent_ips(self):
|
|
646
|
+
"""Determine the list of child IPAddresses and set the parent to self."""
|
|
647
|
+
query = IPAddress.objects.select_for_update().filter(
|
|
648
|
+
ip_version=self.ip_version,
|
|
649
|
+
parent_id=self.parent_id,
|
|
650
|
+
host__gte=self.network,
|
|
651
|
+
host__lte=self.broadcast,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return query.update(parent=self)
|
|
440
655
|
|
|
441
|
-
def
|
|
656
|
+
def supernets(self, direct=False, include_self=False, for_update=False):
|
|
442
657
|
"""
|
|
443
|
-
Return
|
|
444
|
-
|
|
658
|
+
Return supernets of this Prefix.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
direct (bool): Whether to only return the direct ancestor.
|
|
662
|
+
include_self (bool): Whether to include this Prefix in the list of supernets.
|
|
663
|
+
for_update (bool): Lock rows until the end of any subsequent transactions.
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
QuerySet
|
|
445
667
|
"""
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
668
|
+
query = Prefix.objects.all()
|
|
669
|
+
|
|
670
|
+
if for_update:
|
|
671
|
+
query = query.select_for_update()
|
|
672
|
+
|
|
673
|
+
if direct:
|
|
674
|
+
return query.filter(id=self.parent_id)
|
|
675
|
+
|
|
676
|
+
if not include_self:
|
|
677
|
+
query = query.exclude(id=self.id)
|
|
678
|
+
|
|
679
|
+
return query.filter(
|
|
680
|
+
ip_version=self.ip_version,
|
|
681
|
+
prefix_length__lte=self.prefix_length,
|
|
682
|
+
network__lte=self.network,
|
|
683
|
+
broadcast__gte=self.broadcast,
|
|
684
|
+
namespace=self.namespace,
|
|
685
|
+
)
|
|
450
686
|
|
|
451
|
-
def
|
|
687
|
+
def subnets(self, direct=False, include_self=False, for_update=False):
|
|
452
688
|
"""
|
|
453
|
-
Return
|
|
454
|
-
|
|
689
|
+
Return subnets of this Prefix.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
direct (bool): Whether to only return direct descendants.
|
|
693
|
+
include_self (bool): Whether to include this Prefix in the list of subnets.
|
|
694
|
+
for_update (bool): Lock rows until the end of any subsequent transactions.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
QuerySet
|
|
455
698
|
"""
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
699
|
+
query = Prefix.objects.all()
|
|
700
|
+
|
|
701
|
+
if for_update:
|
|
702
|
+
query = query.select_for_update()
|
|
703
|
+
|
|
704
|
+
if direct:
|
|
705
|
+
return query.filter(parent_id=self.id)
|
|
706
|
+
|
|
707
|
+
if not include_self:
|
|
708
|
+
query = query.exclude(id=self.id)
|
|
709
|
+
|
|
710
|
+
return query.filter(
|
|
711
|
+
ip_version=self.ip_version,
|
|
712
|
+
prefix_length__gte=self.prefix_length,
|
|
713
|
+
network__gte=self.network,
|
|
714
|
+
broadcast__lte=self.broadcast,
|
|
715
|
+
namespace=self.namespace,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def is_child_node(self):
|
|
719
|
+
"""
|
|
720
|
+
Returns whether I am a child node.
|
|
721
|
+
"""
|
|
722
|
+
return self.parent is not None
|
|
723
|
+
|
|
724
|
+
def is_leaf_node(self):
|
|
725
|
+
"""
|
|
726
|
+
Returns whether I am leaf node (no children).
|
|
727
|
+
"""
|
|
728
|
+
return not self.children.exists()
|
|
729
|
+
|
|
730
|
+
def is_root_node(self):
|
|
731
|
+
"""
|
|
732
|
+
Returns whether I am a root node (no parent).
|
|
733
|
+
"""
|
|
734
|
+
return self.parent is None
|
|
735
|
+
|
|
736
|
+
def ancestors(self, ascending=False, include_self=False):
|
|
737
|
+
"""
|
|
738
|
+
Return my ancestors descending from larger to smaller prefix lengths.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
ascending (bool): If set, reverses the return order.
|
|
742
|
+
include_self (bool): Whether to include this Prefix in the list of subnets.
|
|
743
|
+
"""
|
|
744
|
+
query = self.supernets(include_self=include_self)
|
|
745
|
+
if ascending:
|
|
746
|
+
query = query.reverse()
|
|
747
|
+
return query
|
|
748
|
+
|
|
749
|
+
def descendants(self, include_self=False):
|
|
750
|
+
"""
|
|
751
|
+
Return all of my children!
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
include_self (bool): Whether to include this Prefix in the list of subnets.
|
|
755
|
+
"""
|
|
756
|
+
return self.subnets(include_self=include_self)
|
|
757
|
+
|
|
758
|
+
@cached_property
|
|
759
|
+
def descendants_count(self):
|
|
760
|
+
"""Display count of descendants."""
|
|
761
|
+
return self.descendants().count()
|
|
762
|
+
|
|
763
|
+
def root(self):
|
|
764
|
+
"""
|
|
765
|
+
Returns the root node (the parent of all of my ancestors).
|
|
766
|
+
"""
|
|
767
|
+
return self.ancestors().first()
|
|
768
|
+
|
|
769
|
+
def siblings(self, include_self=False):
|
|
770
|
+
"""
|
|
771
|
+
Return my siblings. Root nodes are siblings to other root nodes.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
include_self (bool): Whether to include this Prefix in the list of subnets.
|
|
775
|
+
"""
|
|
776
|
+
query = Prefix.objects.filter(parent=self.parent)
|
|
777
|
+
if not include_self:
|
|
778
|
+
query = query.exclude(id=self.id)
|
|
779
|
+
|
|
780
|
+
return query
|
|
460
781
|
|
|
461
782
|
def get_available_prefixes(self):
|
|
462
783
|
"""
|
|
463
784
|
Return all available Prefixes within this prefix as an IPSet.
|
|
464
785
|
"""
|
|
465
786
|
prefix = netaddr.IPSet(self.prefix)
|
|
466
|
-
child_prefixes = netaddr.IPSet([child.prefix for child in self.
|
|
787
|
+
child_prefixes = netaddr.IPSet([child.prefix for child in self.descendants()])
|
|
467
788
|
available_prefixes = prefix - child_prefixes
|
|
468
789
|
|
|
469
790
|
return available_prefixes
|
|
@@ -473,15 +794,15 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
473
794
|
Return all available IPs within this prefix as an IPSet.
|
|
474
795
|
"""
|
|
475
796
|
prefix = netaddr.IPSet(self.prefix)
|
|
476
|
-
child_ips = netaddr.IPSet([ip.address.ip for ip in self.
|
|
797
|
+
child_ips = netaddr.IPSet([ip.address.ip for ip in self.ip_addresses.all()])
|
|
477
798
|
available_ips = prefix - child_ips
|
|
478
799
|
|
|
479
800
|
# IPv6, pool, or IPv4 /31-32 sets are fully usable
|
|
480
801
|
if any(
|
|
481
802
|
[
|
|
482
|
-
self.
|
|
803
|
+
self.ip_version == 6,
|
|
483
804
|
self.type == choices.PrefixTypeChoices.TYPE_POOL,
|
|
484
|
-
self.
|
|
805
|
+
self.ip_version == 4 and self.prefix_length >= 31,
|
|
485
806
|
]
|
|
486
807
|
):
|
|
487
808
|
return available_ips
|
|
@@ -512,7 +833,7 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
512
833
|
available_ips = self.get_available_ips()
|
|
513
834
|
if not available_ips:
|
|
514
835
|
return None
|
|
515
|
-
return f"{next(available_ips.__iter__())}/{self.
|
|
836
|
+
return f"{next(available_ips.__iter__())}/{self.prefix_length}"
|
|
516
837
|
|
|
517
838
|
def get_utilization(self):
|
|
518
839
|
"""Get the child prefix size and parent size.
|
|
@@ -523,16 +844,15 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
523
844
|
UtilizationData (namedtuple): (numerator, denominator)
|
|
524
845
|
"""
|
|
525
846
|
if self.type == choices.PrefixTypeChoices.TYPE_CONTAINER:
|
|
526
|
-
|
|
527
|
-
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
|
847
|
+
child_prefixes = netaddr.IPSet(p.prefix for p in self.descendants())
|
|
528
848
|
return UtilizationData(numerator=child_prefixes.size, denominator=self.prefix.size)
|
|
529
849
|
|
|
530
850
|
else:
|
|
531
851
|
prefix_size = self.prefix.size
|
|
532
852
|
if all(
|
|
533
853
|
[
|
|
534
|
-
self.
|
|
535
|
-
self.
|
|
854
|
+
self.ip_version == 4,
|
|
855
|
+
self.prefix_length < 31,
|
|
536
856
|
self.type != choices.PrefixTypeChoices.TYPE_POOL,
|
|
537
857
|
]
|
|
538
858
|
):
|
|
@@ -550,7 +870,7 @@ class Prefix(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
550
870
|
"statuses",
|
|
551
871
|
"webhooks",
|
|
552
872
|
)
|
|
553
|
-
class IPAddress(PrimaryModel
|
|
873
|
+
class IPAddress(PrimaryModel):
|
|
554
874
|
"""
|
|
555
875
|
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
|
556
876
|
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
|
|
@@ -567,15 +887,23 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
567
887
|
db_index=True,
|
|
568
888
|
help_text="IPv4 or IPv6 host address",
|
|
569
889
|
)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
related_name="ip_addresses",
|
|
890
|
+
mask_length = models.IntegerField(null=False, db_index=True, help_text="Length of the network mask, in bits.")
|
|
891
|
+
status = StatusField(blank=False, null=False)
|
|
892
|
+
role = RoleField(blank=True, null=True)
|
|
893
|
+
parent = models.ForeignKey(
|
|
894
|
+
"ipam.Prefix",
|
|
576
895
|
blank=True,
|
|
577
896
|
null=True,
|
|
578
|
-
|
|
897
|
+
related_name="ip_addresses", # `IPAddress` to use `related_name="ip_addresses"`
|
|
898
|
+
on_delete=models.PROTECT,
|
|
899
|
+
help_text="The parent Prefix of this IPAddress.",
|
|
900
|
+
)
|
|
901
|
+
# ip_version is set internally just like network, and mask_length.
|
|
902
|
+
ip_version = models.IntegerField(
|
|
903
|
+
choices=choices.IPAddressVersionChoices,
|
|
904
|
+
null=True,
|
|
905
|
+
editable=False,
|
|
906
|
+
db_index=True,
|
|
579
907
|
)
|
|
580
908
|
tenant = models.ForeignKey(
|
|
581
909
|
to="tenancy.Tenant",
|
|
@@ -603,18 +931,7 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
603
931
|
)
|
|
604
932
|
description = models.CharField(max_length=200, blank=True)
|
|
605
933
|
|
|
606
|
-
csv_headers = [
|
|
607
|
-
"address",
|
|
608
|
-
"vrf",
|
|
609
|
-
"tenant",
|
|
610
|
-
"status",
|
|
611
|
-
"role",
|
|
612
|
-
"is_primary",
|
|
613
|
-
"dns_name",
|
|
614
|
-
"description",
|
|
615
|
-
]
|
|
616
934
|
clone_fields = [
|
|
617
|
-
"vrf",
|
|
618
935
|
"tenant",
|
|
619
936
|
"status",
|
|
620
937
|
"role",
|
|
@@ -625,13 +942,22 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
625
942
|
objects = BaseManager.from_queryset(IPAddressQuerySet)()
|
|
626
943
|
|
|
627
944
|
class Meta:
|
|
628
|
-
ordering = ("host", "
|
|
945
|
+
ordering = ("ip_version", "host", "mask_length") # address may be non-unique
|
|
629
946
|
verbose_name = "IP address"
|
|
630
947
|
verbose_name_plural = "IP addresses"
|
|
948
|
+
unique_together = ["parent", "host"]
|
|
631
949
|
|
|
632
950
|
def __init__(self, *args, **kwargs):
|
|
633
951
|
address = kwargs.pop("address", None)
|
|
634
|
-
|
|
952
|
+
namespace = kwargs.pop("namespace", None)
|
|
953
|
+
# We don't want users providing their own parent since it will be derived automatically.
|
|
954
|
+
parent = kwargs.pop("parent", None)
|
|
955
|
+
# If namespace wasn't provided, but parent was, we'll use the parent's namespace.
|
|
956
|
+
if namespace is None and parent is not None:
|
|
957
|
+
namespace = parent.namespace
|
|
958
|
+
self._namespace = namespace
|
|
959
|
+
|
|
960
|
+
super().__init__(*args, **kwargs)
|
|
635
961
|
self._deconstruct_address(address)
|
|
636
962
|
|
|
637
963
|
def __str__(self):
|
|
@@ -641,34 +967,15 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
641
967
|
if address:
|
|
642
968
|
if isinstance(address, str):
|
|
643
969
|
address = netaddr.IPNetwork(address)
|
|
644
|
-
# Note that our "broadcast" field is actually the last IP address in this network.
|
|
645
|
-
# This is different from the more accurate technical meaning of a network's broadcast address in 2 cases:
|
|
646
|
-
# 1. For a point-to-point address (IPv4 /31 or IPv6 /127), there are two addresses in the network,
|
|
647
|
-
# and neither one is considered a broadcast address. We store the second address as our "broadcast".
|
|
648
|
-
# 2. For a host prefix (IPv6 /32 or IPv6 /128) there's only one address in the network.
|
|
649
|
-
# We store this address as both the host and the "broadcast".
|
|
650
|
-
# This variance is intentional in both cases as we use the "broadcast" primarily for filtering and grouping
|
|
651
|
-
# of addresses and prefixes, not for packet forwarding. :-)
|
|
652
|
-
broadcast = address.broadcast if address.broadcast else address[-1]
|
|
653
970
|
self.host = str(address.ip)
|
|
654
|
-
self.
|
|
655
|
-
self.
|
|
656
|
-
|
|
657
|
-
def get_absolute_url(self):
|
|
658
|
-
return reverse("ipam:ipaddress", args=[self.pk])
|
|
971
|
+
self.mask_length = address.prefixlen
|
|
972
|
+
self.ip_version = address.version
|
|
659
973
|
|
|
660
974
|
def get_duplicates(self):
|
|
661
975
|
return IPAddress.objects.filter(vrf=self.vrf, host=self.host).exclude(pk=self.pk)
|
|
662
976
|
|
|
663
977
|
# TODO: The current IPAddress model has no appropriate natural key available yet.
|
|
664
|
-
|
|
665
|
-
# but for this model, accessing the natural_key will raise an exception.
|
|
666
|
-
# The below is a hacky way to "remove" the natural_key property from this model class for the time being.
|
|
667
|
-
class AttributeRemover:
|
|
668
|
-
def __get__(self, instance, owner):
|
|
669
|
-
raise AttributeError("IPAddress doesn't yet have a natural key!")
|
|
670
|
-
|
|
671
|
-
natural_key = AttributeRemover()
|
|
978
|
+
natural_key_field_names = ["id"]
|
|
672
979
|
|
|
673
980
|
@classproperty # https://github.com/PyCQA/pylint-django/issues/240
|
|
674
981
|
def STATUS_SLAAC(cls): # pylint: disable=no-self-argument
|
|
@@ -676,7 +983,7 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
676
983
|
cls.__status_slaac = getattr(cls, "__status_slaac", None)
|
|
677
984
|
if cls.__status_slaac is None:
|
|
678
985
|
try:
|
|
679
|
-
cls.__status_slaac = Status.objects.get_for_model(IPAddress).get(
|
|
986
|
+
cls.__status_slaac = Status.objects.get_for_model(IPAddress).get(name="SLAAC")
|
|
680
987
|
except Status.DoesNotExist:
|
|
681
988
|
logger.error("SLAAC Status not found")
|
|
682
989
|
return cls.__status_slaac
|
|
@@ -684,73 +991,36 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
684
991
|
def clean(self):
|
|
685
992
|
super().clean()
|
|
686
993
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if
|
|
691
|
-
raise ValidationError({"address": "
|
|
692
|
-
|
|
693
|
-
# Enforce unique IP space (if applicable)
|
|
694
|
-
if self.role not in IPADDRESS_ROLES_NONUNIQUE and (
|
|
695
|
-
(self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)
|
|
696
|
-
):
|
|
697
|
-
duplicate_ips = self.get_duplicates()
|
|
698
|
-
if duplicate_ips:
|
|
699
|
-
vrf = f"VRF {self.vrf}" if self.vrf else "global table"
|
|
700
|
-
raise ValidationError({"address": f"Duplicate IP address found in {vrf}: {duplicate_ips.first()}"})
|
|
701
|
-
|
|
702
|
-
# TODO: update to work with interface M2M
|
|
703
|
-
# This attribute will have been set by `IPAddressForm.clean()` to indicate that the
|
|
704
|
-
# `primary_ip{version}` field on `self.assigned_object.parent` has been nullified but not yet saved.
|
|
705
|
-
primary_ip_unset_by_form = getattr(self, "_primary_ip_unset_by_form", False)
|
|
706
|
-
|
|
707
|
-
# Check for primary IP assignment that doesn't match the assigned device/VM if and only if
|
|
708
|
-
# "_primary_ip_unset" has not been set by the caller.
|
|
709
|
-
if self.present_in_database and not primary_ip_unset_by_form:
|
|
710
|
-
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
|
711
|
-
if device:
|
|
712
|
-
if getattr(self.assigned_object, "device", None) != device:
|
|
713
|
-
raise ValidationError(
|
|
714
|
-
{"interface": f"IP address is primary for device {device} but not assigned to it!"}
|
|
715
|
-
)
|
|
716
|
-
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
|
717
|
-
if vm:
|
|
718
|
-
if getattr(self.assigned_object, "virtual_machine", None) != vm:
|
|
719
|
-
raise ValidationError(
|
|
720
|
-
{"vminterface": f"IP address is primary for virtual machine {vm} but not assigned to it!"}
|
|
721
|
-
)
|
|
994
|
+
# Validate that host is not being modified
|
|
995
|
+
if self.present_in_database:
|
|
996
|
+
ip_address = IPAddress.objects.get(id=self.id)
|
|
997
|
+
if ip_address.host != self.host:
|
|
998
|
+
raise ValidationError({"address": "Host address cannot be changed once created"})
|
|
722
999
|
|
|
723
1000
|
# Validate IP status selection
|
|
724
|
-
if self.status == IPAddress.STATUS_SLAAC and self.
|
|
1001
|
+
if self.status == IPAddress.STATUS_SLAAC and self.ip_version != 6:
|
|
725
1002
|
raise ValidationError({"status": "Only IPv6 addresses can be assigned SLAAC status"})
|
|
726
1003
|
|
|
727
1004
|
# Force dns_name to lowercase
|
|
728
1005
|
self.dns_name = self.dns_name.lower()
|
|
729
1006
|
|
|
730
|
-
def
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
self.tenant.name if self.tenant else None,
|
|
743
|
-
self.get_status_display(),
|
|
744
|
-
self.role.name if self.role else None,
|
|
745
|
-
str(is_primary),
|
|
746
|
-
self.dns_name,
|
|
747
|
-
self.description,
|
|
748
|
-
)
|
|
1007
|
+
def save(self, *args, **kwargs):
|
|
1008
|
+
if not self.present_in_database:
|
|
1009
|
+
if self._namespace is None:
|
|
1010
|
+
raise ValidationError({"parent": "Namespace could not be determined."})
|
|
1011
|
+
namespace = self._namespace
|
|
1012
|
+
else:
|
|
1013
|
+
namespace = self.parent.namespace
|
|
1014
|
+
|
|
1015
|
+
# Determine the closest parent automatically based on the Namespace.
|
|
1016
|
+
self.parent = Prefix.objects.get_closest_parent(self.host, namespace=namespace)
|
|
1017
|
+
|
|
1018
|
+
super().save(*args, **kwargs)
|
|
749
1019
|
|
|
750
1020
|
@property
|
|
751
1021
|
def address(self):
|
|
752
|
-
if self.host is not None and self.
|
|
753
|
-
cidr = f"{self.host}/{self.
|
|
1022
|
+
if self.host is not None and self.mask_length is not None:
|
|
1023
|
+
cidr = f"{self.host}/{self.mask_length}"
|
|
754
1024
|
return netaddr.IPNetwork(cidr)
|
|
755
1025
|
return None
|
|
756
1026
|
|
|
@@ -758,11 +1028,38 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
758
1028
|
def address(self, address):
|
|
759
1029
|
self._deconstruct_address(address)
|
|
760
1030
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1031
|
+
def ancestors(self, ascending=False):
|
|
1032
|
+
"""
|
|
1033
|
+
Return my ancestors descending from larger to smaller prefix lengths.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
ascending (bool): If set, reverses the return order.
|
|
1037
|
+
"""
|
|
1038
|
+
return self.parent.ancestors(include_self=True, ascending=ascending)
|
|
1039
|
+
|
|
1040
|
+
@cached_property
|
|
1041
|
+
def ancestors_count(self):
|
|
1042
|
+
"""Display count of ancestors."""
|
|
1043
|
+
return self.ancestors().count()
|
|
1044
|
+
|
|
1045
|
+
def root(self):
|
|
1046
|
+
"""
|
|
1047
|
+
Returns the root node (the parent of all of my ancestors).
|
|
1048
|
+
"""
|
|
1049
|
+
return self.ancestors().first()
|
|
1050
|
+
|
|
1051
|
+
def siblings(self, include_self=False):
|
|
1052
|
+
"""
|
|
1053
|
+
Return my siblings that share the same parent Prefix.
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
include_self (bool): Whether to include this IPAddress in the list of siblings.
|
|
1057
|
+
"""
|
|
1058
|
+
query = IPAddress.objects.filter(parent=self.parent)
|
|
1059
|
+
if not include_self:
|
|
1060
|
+
query = query.exclude(id=self.id)
|
|
1061
|
+
|
|
1062
|
+
return query
|
|
766
1063
|
|
|
767
1064
|
# 2.0 TODO: Remove exception, getter, setter below when we can safely deprecate previous properties
|
|
768
1065
|
class NATOutsideMultipleObjectsReturned(MultipleObjectsReturned):
|
|
@@ -776,28 +1073,6 @@ class IPAddress(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
776
1073
|
def __str__(self):
|
|
777
1074
|
return f"Multiple IPAddress objects specify this object (pk: {self.obj.pk}) as nat_inside. Please refer to nat_outside_list."
|
|
778
1075
|
|
|
779
|
-
@property
|
|
780
|
-
def nat_outside(self):
|
|
781
|
-
if self.nat_outside_list.count() > 1:
|
|
782
|
-
raise self.NATOutsideMultipleObjectsReturned(self)
|
|
783
|
-
return self.nat_outside_list.first()
|
|
784
|
-
|
|
785
|
-
@nat_outside.setter
|
|
786
|
-
def nat_outside(self, value):
|
|
787
|
-
if self.nat_outside_list.count() > 1:
|
|
788
|
-
raise self.NATOutsideMultipleObjectsReturned(self)
|
|
789
|
-
return self.nat_outside_list.set([value])
|
|
790
|
-
|
|
791
|
-
def _set_mask_length(self, value):
|
|
792
|
-
"""
|
|
793
|
-
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
|
|
794
|
-
e.g. for bulk editing.
|
|
795
|
-
"""
|
|
796
|
-
if self.address is not None:
|
|
797
|
-
self.prefix_length = value
|
|
798
|
-
|
|
799
|
-
mask_length = property(fset=_set_mask_length)
|
|
800
|
-
|
|
801
1076
|
|
|
802
1077
|
class IPAddressToInterface(BaseModel):
|
|
803
1078
|
ip_address = models.ForeignKey("ipam.IPAddress", on_delete=models.CASCADE, related_name="+")
|
|
@@ -819,19 +1094,11 @@ class IPAddressToInterface(BaseModel):
|
|
|
819
1094
|
is_secondary = models.BooleanField(default=False, help_text="Is secondary address on interface")
|
|
820
1095
|
is_standby = models.BooleanField(default=False, help_text="Is standby address on interface")
|
|
821
1096
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if IPAddressToInterface.objects.filter(
|
|
828
|
-
ip_address=self.ip_address,
|
|
829
|
-
interface=self.interface,
|
|
830
|
-
vm_interface=self.vm_interface,
|
|
831
|
-
).exists():
|
|
832
|
-
raise ValidationError(
|
|
833
|
-
"IPAddressToInterface with this ip_address, interface and vm_interface already exists."
|
|
834
|
-
)
|
|
1097
|
+
class Meta:
|
|
1098
|
+
unique_together = [
|
|
1099
|
+
["ip_address", "interface"],
|
|
1100
|
+
["ip_address", "vm_interface"],
|
|
1101
|
+
]
|
|
835
1102
|
|
|
836
1103
|
def clean(self):
|
|
837
1104
|
super().clean()
|
|
@@ -873,8 +1140,6 @@ class VLANGroup(OrganizationalModel):
|
|
|
873
1140
|
)
|
|
874
1141
|
description = models.CharField(max_length=200, blank=True)
|
|
875
1142
|
|
|
876
|
-
csv_headers = ["name", "slug", "location", "description"]
|
|
877
|
-
|
|
878
1143
|
class Meta:
|
|
879
1144
|
ordering = (
|
|
880
1145
|
"location",
|
|
@@ -897,7 +1162,6 @@ class VLANGroup(OrganizationalModel):
|
|
|
897
1162
|
|
|
898
1163
|
# Validate location
|
|
899
1164
|
if self.location is not None:
|
|
900
|
-
|
|
901
1165
|
if ContentType.objects.get_for_model(self) not in self.location.location_type.content_types.all():
|
|
902
1166
|
raise ValidationError(
|
|
903
1167
|
{"location": f'VLAN groups may not associate to locations of type "{self.location.location_type}".'}
|
|
@@ -906,17 +1170,6 @@ class VLANGroup(OrganizationalModel):
|
|
|
906
1170
|
def __str__(self):
|
|
907
1171
|
return self.name
|
|
908
1172
|
|
|
909
|
-
def get_absolute_url(self):
|
|
910
|
-
return reverse("ipam:vlangroup", args=[self.pk])
|
|
911
|
-
|
|
912
|
-
def to_csv(self):
|
|
913
|
-
return (
|
|
914
|
-
self.name,
|
|
915
|
-
self.slug,
|
|
916
|
-
self.location.name if self.location else None,
|
|
917
|
-
self.description,
|
|
918
|
-
)
|
|
919
|
-
|
|
920
1173
|
def get_next_available_vid(self):
|
|
921
1174
|
"""
|
|
922
1175
|
Return the first available VLAN ID (1-4094) in the group.
|
|
@@ -937,7 +1190,7 @@ class VLANGroup(OrganizationalModel):
|
|
|
937
1190
|
"statuses",
|
|
938
1191
|
"webhooks",
|
|
939
1192
|
)
|
|
940
|
-
class VLAN(PrimaryModel
|
|
1193
|
+
class VLAN(PrimaryModel):
|
|
941
1194
|
"""
|
|
942
1195
|
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094).
|
|
943
1196
|
Each VLAN must be assigned to a Location, however VLAN IDs need not be unique within a Location.
|
|
@@ -965,6 +1218,8 @@ class VLAN(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
965
1218
|
verbose_name="ID", validators=[MinValueValidator(1), MaxValueValidator(4094)]
|
|
966
1219
|
)
|
|
967
1220
|
name = models.CharField(max_length=255, db_index=True)
|
|
1221
|
+
status = StatusField(blank=False, null=False)
|
|
1222
|
+
role = RoleField(blank=True, null=True)
|
|
968
1223
|
tenant = models.ForeignKey(
|
|
969
1224
|
to="tenancy.Tenant",
|
|
970
1225
|
on_delete=models.PROTECT,
|
|
@@ -974,16 +1229,6 @@ class VLAN(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
974
1229
|
)
|
|
975
1230
|
description = models.CharField(max_length=200, blank=True)
|
|
976
1231
|
|
|
977
|
-
csv_headers = [
|
|
978
|
-
"location",
|
|
979
|
-
"vlan_group",
|
|
980
|
-
"vid",
|
|
981
|
-
"name",
|
|
982
|
-
"tenant",
|
|
983
|
-
"status",
|
|
984
|
-
"role",
|
|
985
|
-
"description",
|
|
986
|
-
]
|
|
987
1232
|
clone_fields = [
|
|
988
1233
|
"location",
|
|
989
1234
|
"vlan_group",
|
|
@@ -993,6 +1238,8 @@ class VLAN(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
993
1238
|
"description",
|
|
994
1239
|
]
|
|
995
1240
|
|
|
1241
|
+
natural_key_field_names = ["vid", "vlan_group"]
|
|
1242
|
+
|
|
996
1243
|
class Meta:
|
|
997
1244
|
ordering = (
|
|
998
1245
|
"location",
|
|
@@ -1011,15 +1258,11 @@ class VLAN(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
1011
1258
|
def __str__(self):
|
|
1012
1259
|
return self.display or super().__str__()
|
|
1013
1260
|
|
|
1014
|
-
def get_absolute_url(self):
|
|
1015
|
-
return reverse("ipam:vlan", args=[self.pk])
|
|
1016
|
-
|
|
1017
1261
|
def clean(self):
|
|
1018
1262
|
super().clean()
|
|
1019
1263
|
|
|
1020
1264
|
# Validate location
|
|
1021
1265
|
if self.location is not None:
|
|
1022
|
-
|
|
1023
1266
|
if ContentType.objects.get_for_model(self) not in self.location.location_type.content_types.all():
|
|
1024
1267
|
raise ValidationError(
|
|
1025
1268
|
{"location": f'VLANs may not associate to locations of type "{self.location.location_type}".'}
|
|
@@ -1038,18 +1281,6 @@ class VLAN(PrimaryModel, StatusModel, RoleModelMixin):
|
|
|
1038
1281
|
}
|
|
1039
1282
|
)
|
|
1040
1283
|
|
|
1041
|
-
def to_csv(self):
|
|
1042
|
-
return (
|
|
1043
|
-
self.location.name if self.location else None,
|
|
1044
|
-
self.vlan_group.name if self.vlan_group else None,
|
|
1045
|
-
self.vid,
|
|
1046
|
-
self.name,
|
|
1047
|
-
self.tenant.name if self.tenant else None,
|
|
1048
|
-
self.get_status_display(),
|
|
1049
|
-
self.role.name if self.role else None,
|
|
1050
|
-
self.description,
|
|
1051
|
-
)
|
|
1052
|
-
|
|
1053
1284
|
@property
|
|
1054
1285
|
def display(self):
|
|
1055
1286
|
return f"{self.name} ({self.vid})"
|
|
@@ -1110,15 +1341,6 @@ class Service(PrimaryModel):
|
|
|
1110
1341
|
)
|
|
1111
1342
|
description = models.CharField(max_length=200, blank=True)
|
|
1112
1343
|
|
|
1113
|
-
csv_headers = [
|
|
1114
|
-
"device",
|
|
1115
|
-
"virtual_machine",
|
|
1116
|
-
"name",
|
|
1117
|
-
"protocol",
|
|
1118
|
-
"ports",
|
|
1119
|
-
"description",
|
|
1120
|
-
]
|
|
1121
|
-
|
|
1122
1344
|
class Meta:
|
|
1123
1345
|
ordering = (
|
|
1124
1346
|
"protocol",
|
|
@@ -1128,13 +1350,12 @@ class Service(PrimaryModel):
|
|
|
1128
1350
|
def __str__(self):
|
|
1129
1351
|
return f"{self.name} ({self.get_protocol_display()}/{self.port_list})"
|
|
1130
1352
|
|
|
1131
|
-
def get_absolute_url(self):
|
|
1132
|
-
return reverse("ipam:service", args=[self.pk])
|
|
1133
|
-
|
|
1134
1353
|
@property
|
|
1135
1354
|
def parent(self):
|
|
1136
1355
|
return self.device or self.virtual_machine
|
|
1137
1356
|
|
|
1357
|
+
natural_key_field_names = ["name", "device", "virtual_machine"]
|
|
1358
|
+
|
|
1138
1359
|
def clean(self):
|
|
1139
1360
|
super().clean()
|
|
1140
1361
|
|
|
@@ -1144,16 +1365,6 @@ class Service(PrimaryModel):
|
|
|
1144
1365
|
if not self.device and not self.virtual_machine:
|
|
1145
1366
|
raise ValidationError("A service must be associated with either a device or a virtual machine.")
|
|
1146
1367
|
|
|
1147
|
-
def to_csv(self):
|
|
1148
|
-
return (
|
|
1149
|
-
self.device.name if self.device else None,
|
|
1150
|
-
self.virtual_machine.name if self.virtual_machine else None,
|
|
1151
|
-
self.name,
|
|
1152
|
-
self.get_protocol_display(),
|
|
1153
|
-
self.ports,
|
|
1154
|
-
self.description,
|
|
1155
|
-
)
|
|
1156
|
-
|
|
1157
1368
|
@property
|
|
1158
1369
|
def port_list(self):
|
|
1159
1370
|
return array_to_string(self.ports)
|