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.
Files changed (1029) hide show
  1. nautobot/__init__.py +1 -5
  2. nautobot/apps/api.py +6 -8
  3. nautobot/apps/forms.py +0 -2
  4. nautobot/apps/ui.py +0 -8
  5. nautobot/circuits/api/serializers.py +9 -119
  6. nautobot/circuits/api/urls.py +1 -1
  7. nautobot/circuits/api/views.py +0 -1
  8. nautobot/circuits/choices.py +0 -2
  9. nautobot/circuits/filters.py +7 -6
  10. nautobot/circuits/forms.py +3 -73
  11. nautobot/circuits/migrations/0001_initial_part_1.py +0 -1
  12. nautobot/circuits/migrations/0002_initial_part_2.py +0 -1
  13. nautobot/circuits/migrations/0003_auto_slug.py +0 -1
  14. nautobot/circuits/migrations/0004_increase_provider_account_length.py +0 -1
  15. nautobot/circuits/migrations/0005_providernetwork.py +0 -1
  16. nautobot/circuits/migrations/0006_cache_circuit_terminations.py +0 -1
  17. nautobot/circuits/migrations/0007_circuitterminations_primary_model.py +0 -1
  18. nautobot/circuits/migrations/0008_add_natural_indexing.py +0 -1
  19. nautobot/circuits/migrations/0009_circuittermination_location.py +0 -1
  20. nautobot/circuits/migrations/0010_rename_foreign_keys_and_related_names.py +0 -1
  21. nautobot/circuits/migrations/0011_remove_site_foreign_key_from_circuit_termination_class.py +0 -1
  22. nautobot/circuits/migrations/0012_created_datetime.py +0 -1
  23. nautobot/circuits/migrations/0013_alter_circuittermination__path.py +0 -1
  24. nautobot/circuits/migrations/0014_related_name_changes.py +1 -2
  25. nautobot/circuits/migrations/0015_remove_circuittype_provider_slug.py +20 -0
  26. nautobot/circuits/migrations/0016_tagsfield.py +34 -0
  27. nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
  28. nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
  29. nautobot/circuits/models.py +3 -93
  30. nautobot/circuits/navigation.py +14 -69
  31. nautobot/circuits/signals.py +0 -2
  32. nautobot/circuits/tables.py +42 -5
  33. nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -1
  34. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -1
  35. nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -1
  36. nautobot/circuits/templates/circuits/provider_create.html +0 -1
  37. nautobot/circuits/templates/circuits/provider_retrieve.html +1 -1
  38. nautobot/circuits/tests/integration/test_relationships.py +13 -16
  39. nautobot/circuits/tests/test_api.py +13 -43
  40. nautobot/circuits/tests/test_filters.py +20 -15
  41. nautobot/circuits/tests/test_models.py +7 -3
  42. nautobot/circuits/tests/test_views.py +57 -67
  43. nautobot/circuits/views.py +18 -9
  44. nautobot/core/api/__init__.py +8 -2
  45. nautobot/core/api/authentication.py +0 -3
  46. nautobot/core/api/fields.py +15 -6
  47. nautobot/core/api/filter_backends.py +3 -2
  48. nautobot/core/api/metadata.py +237 -30
  49. nautobot/core/api/mixins.py +94 -0
  50. nautobot/core/api/pagination.py +3 -3
  51. nautobot/core/api/parsers.py +154 -0
  52. nautobot/core/api/renderers.py +153 -2
  53. nautobot/core/api/schema.py +47 -3
  54. nautobot/core/api/serializers.py +377 -37
  55. nautobot/core/api/urls.py +11 -3
  56. nautobot/core/api/utils.py +174 -2
  57. nautobot/core/api/versioning.py +32 -10
  58. nautobot/core/api/views.py +266 -75
  59. nautobot/core/apps/__init__.py +138 -221
  60. nautobot/core/celery/__init__.py +112 -41
  61. nautobot/core/celery/backends.py +19 -13
  62. nautobot/core/celery/control.py +46 -0
  63. nautobot/core/celery/encoders.py +53 -0
  64. nautobot/core/celery/log.py +38 -0
  65. nautobot/core/celery/schedulers.py +23 -4
  66. nautobot/core/celery/task.py +1 -16
  67. nautobot/core/checks.py +0 -27
  68. nautobot/core/choices.py +21 -113
  69. nautobot/core/{cli.py → cli/__init__.py} +1 -2
  70. nautobot/core/cli/__main__.py +3 -0
  71. nautobot/core/constants.py +25 -43
  72. nautobot/core/context_processors.py +12 -0
  73. nautobot/core/filters.py +2 -2
  74. nautobot/core/forms/__init__.py +0 -4
  75. nautobot/core/forms/fields.py +39 -68
  76. nautobot/core/forms/forms.py +27 -27
  77. nautobot/core/forms/utils.py +7 -59
  78. nautobot/core/forms/widgets.py +0 -1
  79. nautobot/core/graphql/__init__.py +2 -2
  80. nautobot/core/graphql/schema.py +4 -27
  81. nautobot/core/jobs/__init__.py +75 -0
  82. nautobot/core/management/commands/build_ui.py +255 -0
  83. nautobot/core/management/commands/celery.py +0 -1
  84. nautobot/core/management/commands/generate_test_data.py +18 -13
  85. nautobot/core/management/commands/post_upgrade.py +24 -24
  86. nautobot/core/management/commands/validate_models.py +0 -1
  87. nautobot/core/middleware.py +0 -1
  88. nautobot/core/models/__init__.py +26 -1
  89. nautobot/core/models/fields.py +24 -5
  90. nautobot/core/models/generics.py +2 -46
  91. nautobot/core/models/managers.py +5 -0
  92. nautobot/core/models/name_color_content_types.py +1 -19
  93. nautobot/core/models/tree_queries.py +14 -4
  94. nautobot/core/models/utils.py +9 -10
  95. nautobot/core/models/validators.py +17 -8
  96. nautobot/core/releases.py +8 -10
  97. nautobot/core/settings.py +81 -53
  98. nautobot/core/tables.py +5 -5
  99. nautobot/core/tasks.py +4 -7
  100. nautobot/core/templates/base.html +1 -49
  101. nautobot/core/templates/base_django.html +49 -0
  102. nautobot/core/templates/base_react.html +55 -0
  103. nautobot/core/templates/buttons/export.html +6 -4
  104. nautobot/core/templates/generic/object_bulk_create.html +10 -21
  105. nautobot/core/templates/generic/object_list.html +4 -1
  106. nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
  107. nautobot/core/templates/inc/footer.html +1 -0
  108. nautobot/core/templates/inc/javascript.html +0 -14
  109. nautobot/core/templates/inc/nav_menu.html +28 -33
  110. nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
  111. nautobot/core/templates/inc/relationships_table_rows.html +2 -2
  112. nautobot/core/templates/nautobot_config.py.j2 +8 -25
  113. nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
  114. nautobot/core/templates/rest_framework/api.html +8 -0
  115. nautobot/core/templatetags/buttons.py +32 -29
  116. nautobot/core/templatetags/helpers.py +1 -1
  117. nautobot/core/testing/__init__.py +47 -44
  118. nautobot/core/testing/api.py +365 -47
  119. nautobot/core/testing/filters.py +12 -7
  120. nautobot/core/testing/integration.py +1 -1
  121. nautobot/core/testing/migrations.py +2 -0
  122. nautobot/core/testing/mixins.py +22 -12
  123. nautobot/core/testing/schema.py +2 -1
  124. nautobot/core/testing/views.py +28 -51
  125. nautobot/core/tests/integration/test_filters.py +17 -8
  126. nautobot/core/tests/integration/test_navbar.py +11 -34
  127. nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
  128. nautobot/core/tests/nautobot_config.py +2 -3
  129. nautobot/core/tests/runner.py +0 -1
  130. nautobot/core/tests/test_api.py +290 -24
  131. nautobot/core/tests/test_authentication.py +57 -14
  132. nautobot/core/tests/test_checks.py +0 -7
  133. nautobot/core/tests/test_choices.py +0 -1
  134. nautobot/core/tests/test_filters.py +117 -110
  135. nautobot/core/tests/test_forms.py +47 -110
  136. nautobot/core/tests/test_graphql.py +158 -135
  137. nautobot/core/tests/test_logging.py +4 -1
  138. nautobot/core/tests/test_managers.py +3 -5
  139. nautobot/core/tests/test_models.py +2 -0
  140. nautobot/core/tests/test_ordering.py +0 -2
  141. nautobot/core/tests/test_paginator.py +3 -1
  142. nautobot/core/tests/test_releases.py +12 -12
  143. nautobot/core/tests/test_templatetags_helpers.py +7 -4
  144. nautobot/core/tests/test_utils.py +112 -78
  145. nautobot/core/tests/test_views.py +12 -17
  146. nautobot/core/tests/test_views_utils.py +6 -9
  147. nautobot/core/utils/data.py +17 -0
  148. nautobot/core/utils/deprecation.py +13 -20
  149. nautobot/core/utils/filtering.py +53 -9
  150. nautobot/core/utils/git.py +12 -4
  151. nautobot/core/utils/lookup.py +3 -1
  152. nautobot/core/utils/requests.py +23 -116
  153. nautobot/core/views/__init__.py +1 -2
  154. nautobot/core/views/generic.py +131 -119
  155. nautobot/core/views/mixins.py +53 -62
  156. nautobot/core/views/paginator.py +0 -1
  157. nautobot/core/views/renderers.py +14 -12
  158. nautobot/core/views/utils.py +87 -4
  159. nautobot/dcim/api/serializers.py +160 -672
  160. nautobot/dcim/api/urls.py +1 -1
  161. nautobot/dcim/api/views.py +7 -46
  162. nautobot/dcim/choices.py +2 -25
  163. nautobot/dcim/elevations.py +0 -1
  164. nautobot/dcim/factory.py +15 -4
  165. nautobot/dcim/filters/__init__.py +42 -13
  166. nautobot/dcim/form_mixins.py +1 -27
  167. nautobot/dcim/forms.py +58 -797
  168. nautobot/dcim/management/commands/trace_paths.py +0 -1
  169. nautobot/dcim/migrations/0001_initial_part_1.py +0 -1
  170. nautobot/dcim/migrations/0002_initial_part_2.py +0 -1
  171. nautobot/dcim/migrations/0003_initial_part_3.py +0 -1
  172. nautobot/dcim/migrations/0004_initial_part_4.py +0 -1
  173. nautobot/dcim/migrations/0005_device_local_context_schema.py +0 -1
  174. nautobot/dcim/migrations/0006_auto_slug.py +0 -1
  175. nautobot/dcim/migrations/0007_device_secrets_group.py +0 -1
  176. nautobot/dcim/migrations/0008_increase_all_serial_lengths.py +0 -1
  177. nautobot/dcim/migrations/0009_add_natural_indexing.py +0 -1
  178. nautobot/dcim/migrations/0010_interface_status.py +0 -1
  179. nautobot/dcim/migrations/0011_interface_status_data_migration.py +0 -1
  180. nautobot/dcim/migrations/0012_interface_parent_bridge.py +0 -1
  181. nautobot/dcim/migrations/0013_location_location_type.py +0 -1
  182. nautobot/dcim/migrations/0014_location_status_data_migration.py +0 -1
  183. nautobot/dcim/migrations/0015_device_components__changeloggedmodel.py +0 -1
  184. nautobot/dcim/migrations/0016_device_components__timestamp_data_migration.py +0 -1
  185. nautobot/dcim/migrations/0017_locationtype_nestable.py +0 -1
  186. nautobot/dcim/migrations/0018_device_redundancy_group.py +0 -1
  187. nautobot/dcim/migrations/0019_device_redundancy_group_data_migration.py +0 -1
  188. nautobot/dcim/migrations/0020_move_site_fields_to_location_model.py +0 -1
  189. nautobot/dcim/migrations/0021_mptt_to_tree_queries.py +0 -1
  190. nautobot/dcim/migrations/0022_interface_mac_address_data_migration.py +0 -1
  191. nautobot/dcim/migrations/0023_alter_interface_mac_address.py +0 -1
  192. nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -2
  193. nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -14
  194. nautobot/dcim/migrations/0026_rename_device_and_rack_role.py +0 -1
  195. nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -2
  196. nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -2
  197. nautobot/dcim/migrations/0029_add_tree_managers_and_foreign_keys_pre_data_migration.py +0 -1
  198. nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
  199. nautobot/dcim/migrations/0031_rename_path_end_point_related_name.py +0 -1
  200. nautobot/dcim/migrations/0032_remove_site_foreign_key_from_dcim_models.py +0 -1
  201. nautobot/dcim/migrations/0033_created_datetime.py +0 -1
  202. nautobot/dcim/migrations/0034_fixup_fks_and_related_names.py +0 -1
  203. nautobot/dcim/migrations/0035_related_name_changes.py +1 -2
  204. nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -2
  205. nautobot/dcim/migrations/0037_interface_ip_addresses_m2m.py +0 -1
  206. nautobot/dcim/migrations/0038_alter_location_managers.py +0 -1
  207. nautobot/dcim/migrations/0039_remove_slug.py +24 -0
  208. nautobot/dcim/migrations/0040_tagsfield.py +109 -0
  209. nautobot/dcim/migrations/0041_ipam__namespaces.py +25 -0
  210. nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
  211. nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
  212. nautobot/dcim/models/cables.py +4 -35
  213. nautobot/dcim/models/device_component_templates.py +7 -2
  214. nautobot/dcim/models/device_components.py +26 -203
  215. nautobot/dcim/models/devices.py +30 -152
  216. nautobot/dcim/models/locations.py +3 -64
  217. nautobot/dcim/models/power.py +3 -51
  218. nautobot/dcim/models/racks.py +7 -86
  219. nautobot/dcim/navigation.py +141 -467
  220. nautobot/dcim/signals.py +0 -2
  221. nautobot/dcim/tables/devices.py +8 -5
  222. nautobot/dcim/tables/devicetypes.py +1 -1
  223. nautobot/dcim/tables/locations.py +2 -2
  224. nautobot/dcim/tables/power.py +2 -2
  225. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
  226. nautobot/dcim/templates/dcim/device.html +15 -4
  227. nautobot/dcim/templates/dcim/device_edit.html +6 -0
  228. nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +0 -1
  229. nautobot/dcim/templates/dcim/devicetype.html +2 -2
  230. nautobot/dcim/templates/dcim/interface.html +4 -0
  231. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
  232. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  233. nautobot/dcim/templates/dcim/location.html +16 -1
  234. nautobot/dcim/templates/dcim/locationtype.html +15 -0
  235. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
  236. nautobot/dcim/templates/dcim/rackgroup.html +0 -12
  237. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  238. nautobot/dcim/tests/test_api.py +202 -130
  239. nautobot/dcim/tests/test_cablepaths.py +47 -42
  240. nautobot/dcim/tests/test_filters.py +156 -134
  241. nautobot/dcim/tests/test_forms.py +12 -213
  242. nautobot/dcim/tests/test_graphql.py +8 -3
  243. nautobot/dcim/tests/test_migrations.py +6 -11
  244. nautobot/dcim/tests/test_models.py +208 -158
  245. nautobot/dcim/tests/test_natural_ordering.py +12 -14
  246. nautobot/dcim/tests/test_signals.py +7 -4
  247. nautobot/dcim/tests/test_views.py +270 -264
  248. nautobot/dcim/urls.py +21 -26
  249. nautobot/dcim/views.py +14 -156
  250. nautobot/docs/additional-features/caching.md +6 -87
  251. nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
  252. nautobot/docs/additional-features/jobs.md +179 -197
  253. nautobot/docs/administration/nautobot-server.md +9 -24
  254. nautobot/docs/administration/nautobot-shell.md +6 -6
  255. nautobot/docs/administration/replicating-nautobot.md +0 -10
  256. nautobot/docs/configuration/index.md +9 -9
  257. nautobot/docs/configuration/optional-settings.md +32 -61
  258. nautobot/docs/configuration/required-settings.md +11 -52
  259. nautobot/docs/development/application-registry.md +2 -13
  260. nautobot/docs/development/best-practices.md +2 -1
  261. nautobot/docs/development/docker-compose-advanced-use-cases.md +1 -1
  262. nautobot/docs/development/extending-models.md +15 -17
  263. nautobot/docs/development/generic-views.md +0 -2
  264. nautobot/docs/development/getting-started.md +56 -6
  265. nautobot/docs/development/navigation-menu.md +22 -93
  266. nautobot/docs/development/react-ui.md +105 -0
  267. nautobot/docs/development/release-checklist.md +3 -3
  268. nautobot/docs/development/role-internals.md +1 -3
  269. nautobot/docs/development/style-guide.md +6 -4
  270. nautobot/docs/development/templates.md +2 -1
  271. nautobot/docs/docker/index.md +16 -14
  272. nautobot/docs/index.md +7 -3
  273. nautobot/docs/installation/index.md +4 -1
  274. nautobot/docs/installation/migrating-from-netbox.md +12 -43
  275. nautobot/docs/installation/migrating-from-postgresql.md +1 -1
  276. nautobot/docs/installation/nautobot.md +1 -1
  277. nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  278. nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  279. nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  280. nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  281. nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
  282. nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  283. nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  284. nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  285. nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  286. nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  287. nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  288. nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  289. nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  290. nautobot/docs/installation/upgrading-from-nautobot-v1.md +190 -636
  291. nautobot/docs/installation/upgrading.md +5 -2
  292. nautobot/docs/models/dcim/device.md +3 -0
  293. nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
  294. nautobot/docs/models/extras/computedfield.md +4 -4
  295. nautobot/docs/models/extras/dynamicgroup.md +9 -9
  296. nautobot/docs/models/extras/gitrepository.md +3 -0
  297. nautobot/docs/models/extras/job.md +1 -0
  298. nautobot/docs/models/extras/jobbutton.md +18 -13
  299. nautobot/docs/models/extras/jobhook.md +7 -4
  300. nautobot/docs/models/extras/jobresult.md +6 -2
  301. nautobot/docs/models/extras/relationship.md +2 -2
  302. nautobot/docs/models/extras/status.md +6 -19
  303. nautobot/docs/models/ipam/ipaddress.md +3 -0
  304. nautobot/docs/models/ipam/vrf.md +0 -3
  305. nautobot/docs/models/virtualization/virtualmachine.md +3 -0
  306. nautobot/docs/plugins/development.md +92 -24
  307. nautobot/docs/release-notes/version-1.5.md +96 -0
  308. nautobot/docs/release-notes/version-2.0.md +216 -0
  309. nautobot/docs/requirements.txt +5 -4
  310. nautobot/docs/rest-api/overview.md +384 -215
  311. nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
  312. nautobot/extras/admin.py +3 -5
  313. nautobot/extras/api/customfields.py +15 -39
  314. nautobot/extras/api/fields.py +0 -11
  315. nautobot/extras/api/mixins.py +45 -0
  316. nautobot/extras/api/relationships.py +63 -159
  317. nautobot/extras/api/serializers.py +165 -706
  318. nautobot/extras/api/urls.py +1 -1
  319. nautobot/extras/api/views.py +295 -282
  320. nautobot/extras/apps.py +4 -7
  321. nautobot/extras/choices.py +11 -22
  322. nautobot/extras/constants.py +9 -3
  323. nautobot/extras/datasources/__init__.py +2 -0
  324. nautobot/extras/datasources/git.py +135 -186
  325. nautobot/extras/datasources/registry.py +25 -35
  326. nautobot/extras/factory.py +1 -3
  327. nautobot/extras/filters/__init__.py +49 -47
  328. nautobot/extras/filters/mixins.py +10 -8
  329. nautobot/extras/forms/forms.py +72 -148
  330. nautobot/extras/forms/mixins.py +34 -57
  331. nautobot/extras/health_checks.py +0 -33
  332. nautobot/extras/jobs.py +387 -566
  333. nautobot/extras/management/__init__.py +55 -48
  334. nautobot/extras/management/commands/renaturalize.py +0 -1
  335. nautobot/extras/management/commands/runjob.py +24 -62
  336. nautobot/extras/management/commands/webhook_receiver.py +0 -1
  337. nautobot/extras/managers.py +30 -7
  338. nautobot/extras/migrations/0001_initial_part_1.py +0 -1
  339. nautobot/extras/migrations/0002_initial_part_2.py +0 -1
  340. nautobot/extras/migrations/0003_initial_part_3.py +0 -1
  341. nautobot/extras/migrations/0004_populate_default_status_records.py +0 -1
  342. nautobot/extras/migrations/0005_configcontext_device_types.py +0 -1
  343. nautobot/extras/migrations/0006_graphqlquery.py +0 -1
  344. nautobot/extras/migrations/0007_configcontextschema.py +0 -1
  345. nautobot/extras/migrations/0008_jobresult__custom_field_data.py +0 -1
  346. nautobot/extras/migrations/0009_computedfield.py +0 -1
  347. nautobot/extras/migrations/0010_change_cf_validation_max_min_field_to_bigint.py +0 -1
  348. nautobot/extras/migrations/0011_fileattachment_fileproxy.py +0 -1
  349. nautobot/extras/migrations/0012_healthchecktestmodel.py +0 -1
  350. nautobot/extras/migrations/0013_default_fallback_value_computedfield.py +0 -1
  351. nautobot/extras/migrations/0014_auto_slug.py +0 -1
  352. nautobot/extras/migrations/0015_scheduled_job.py +0 -1
  353. nautobot/extras/migrations/0016_secret.py +0 -1
  354. nautobot/extras/migrations/0017_joblogentry.py +0 -1
  355. nautobot/extras/migrations/0018_joblog_data_migration.py +0 -2
  356. nautobot/extras/migrations/0019_joblogentry__meta_options__related_name.py +0 -1
  357. nautobot/extras/migrations/0020_customfield_changelog.py +0 -1
  358. nautobot/extras/migrations/0021_customfield_changelog_data.py +0 -1
  359. nautobot/extras/migrations/0022_objectchange_object_datav2.py +0 -1
  360. nautobot/extras/migrations/0023_job_model.py +0 -1
  361. nautobot/extras/migrations/0024_job_data_migration.py +0 -1
  362. nautobot/extras/migrations/0025_add_advanced_ui_boolean_to_customfield_conputedfield_and_relationship.py +0 -1
  363. nautobot/extras/migrations/0026_job_add_gitrepository_fk.py +0 -1
  364. nautobot/extras/migrations/0027_job_gitrepository_data_migration.py +0 -1
  365. nautobot/extras/migrations/0028_job_reduce_source.py +0 -1
  366. nautobot/extras/migrations/0029_dynamicgroup.py +0 -1
  367. nautobot/extras/migrations/0030_webhook_alter_unique_together.py +0 -1
  368. nautobot/extras/migrations/0031_tag_content_types.py +0 -1
  369. nautobot/extras/migrations/0032_tag_content_types_data_migration.py +0 -1
  370. nautobot/extras/migrations/0033_add__optimized_indexing.py +0 -1
  371. nautobot/extras/migrations/0034_alter_fileattachment_mimetype.py +0 -1
  372. nautobot/extras/migrations/0035_scheduledjob_crontab.py +0 -1
  373. nautobot/extras/migrations/0036_job_add_has_sensitive_variables.py +0 -1
  374. nautobot/extras/migrations/0037_configcontextschema__remove_name_unique__create_constraint_unique_name_owner.py +0 -1
  375. nautobot/extras/migrations/0038_configcontext_locations.py +0 -1
  376. nautobot/extras/migrations/0039_objectchange__add_change_context.py +0 -1
  377. nautobot/extras/migrations/0040_dynamicgroup__dynamicgroupmembership.py +0 -1
  378. nautobot/extras/migrations/0041_jobresult_job_kwargs.py +0 -1
  379. nautobot/extras/migrations/0042_job__add_is_job_hook_receiver.py +0 -1
  380. nautobot/extras/migrations/0043_note.py +0 -1
  381. nautobot/extras/migrations/0044_add_job_hook.py +0 -1
  382. nautobot/extras/migrations/0045_add_custom_field_slug.py +0 -1
  383. nautobot/extras/migrations/0046_populate_custom_field_slug_label.py +0 -1
  384. nautobot/extras/migrations/0047_enforce_custom_field_slug.py +0 -1
  385. nautobot/extras/migrations/0048_alter_objectchange_change_context_detail.py +0 -1
  386. nautobot/extras/migrations/0049_alter_tag_slug.py +0 -1
  387. nautobot/extras/migrations/0050_customfield_grouping.py +0 -1
  388. nautobot/extras/migrations/0051_add_job_task_queues.py +0 -1
  389. nautobot/extras/migrations/0052_configcontext_device_redundancy_groups.py +0 -1
  390. nautobot/extras/migrations/0053_relationship_required_on.py +0 -1
  391. nautobot/extras/migrations/0054_scheduledjob_kwargs_request_user_change.py +0 -1
  392. nautobot/extras/migrations/0055_configcontext_dynamic_groups.py +0 -1
  393. nautobot/extras/migrations/0056_objectchange_add_reverse_time_idx.py +0 -1
  394. nautobot/extras/migrations/0057_jobbutton.py +0 -1
  395. nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
  396. nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -2
  397. nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -2
  398. nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -8
  399. nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -33
  400. nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -2
  401. nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -2
  402. nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
  403. nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -2
  404. nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -3
  405. nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -2
  406. 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
  407. nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
  408. nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -2
  409. nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
  410. nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -10
  411. nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -2
  412. nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
  413. nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
  414. 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
  415. nautobot/extras/migrations/0078_remove_slug.py +45 -0
  416. nautobot/extras/migrations/0079_tagsfield.py +28 -0
  417. nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
  418. nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
  419. nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
  420. nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
  421. nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
  422. nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
  423. nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
  424. nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
  425. nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
  426. nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
  427. nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
  428. nautobot/extras/models/__init__.py +2 -3
  429. nautobot/extras/models/change_logging.py +0 -36
  430. nautobot/extras/models/customfields.py +39 -33
  431. nautobot/extras/models/datasources.py +48 -50
  432. nautobot/extras/models/groups.py +5 -12
  433. nautobot/extras/models/jobs.py +190 -323
  434. nautobot/extras/models/mixins.py +0 -71
  435. nautobot/extras/models/models.py +1 -22
  436. nautobot/extras/models/relationships.py +20 -21
  437. nautobot/extras/models/roles.py +0 -23
  438. nautobot/extras/models/secrets.py +2 -31
  439. nautobot/extras/models/statuses.py +6 -5
  440. nautobot/extras/models/tags.py +2 -17
  441. nautobot/extras/navigation.py +89 -307
  442. nautobot/extras/plugins/__init__.py +3 -121
  443. nautobot/extras/plugins/utils.py +0 -3
  444. nautobot/extras/plugins/validators.py +5 -4
  445. nautobot/extras/plugins/views.py +16 -4
  446. nautobot/extras/querysets.py +1 -7
  447. nautobot/extras/registry.py +3 -0
  448. nautobot/extras/signals.py +26 -60
  449. nautobot/extras/tables.py +42 -49
  450. nautobot/extras/tasks.py +0 -12
  451. nautobot/extras/templates/extras/configcontext.html +1 -1
  452. nautobot/extras/templates/extras/configcontextschema.html +16 -1
  453. nautobot/extras/templates/extras/customfield.html +0 -13
  454. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -1
  455. nautobot/extras/templates/extras/gitrepository.html +3 -3
  456. nautobot/extras/templates/extras/inc/jobresult.html +10 -0
  457. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  458. nautobot/extras/templates/extras/job.html +35 -25
  459. nautobot/extras/templates/extras/job_approval_request.html +15 -30
  460. nautobot/extras/templates/extras/job_detail.html +13 -31
  461. nautobot/extras/templates/extras/job_edit.html +14 -17
  462. nautobot/extras/templates/extras/jobresult.html +24 -6
  463. nautobot/extras/templates/extras/objectchange_list.html +1 -1
  464. nautobot/extras/templates/extras/scheduledjob.html +2 -2
  465. nautobot/extras/templates/extras/secret.html +28 -0
  466. nautobot/extras/templates/extras/secret_edit.html +0 -1
  467. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -1
  468. nautobot/extras/templatetags/custom_links.py +0 -2
  469. nautobot/extras/templatetags/job_buttons.py +1 -0
  470. nautobot/extras/templatetags/plugins.py +0 -1
  471. nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
  472. nautobot/extras/test_jobs/atomic_transaction.py +53 -0
  473. nautobot/extras/test_jobs/dry_run.py +29 -0
  474. nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
  475. nautobot/extras/test_jobs/duplicate_name2.py +9 -0
  476. nautobot/extras/test_jobs/fail.py +23 -0
  477. nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
  478. nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
  479. nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
  480. nautobot/extras/test_jobs/file_upload_pass.py +25 -0
  481. nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
  482. nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
  483. nautobot/extras/test_jobs/job_button_receiver.py +28 -0
  484. nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
  485. nautobot/extras/test_jobs/job_variables.py +88 -0
  486. nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
  487. nautobot/extras/test_jobs/log_redaction.py +20 -0
  488. nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
  489. nautobot/extras/test_jobs/modify_db.py +25 -0
  490. nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
  491. nautobot/extras/test_jobs/object_var_optional.py +21 -0
  492. nautobot/extras/test_jobs/object_var_required.py +21 -0
  493. nautobot/extras/test_jobs/object_vars.py +26 -0
  494. nautobot/extras/test_jobs/pass.py +25 -0
  495. nautobot/extras/test_jobs/profiling.py +32 -0
  496. nautobot/extras/test_jobs/read_only_job.py +15 -0
  497. nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
  498. 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
  499. nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
  500. nautobot/extras/tests/integration/__init__.py +3 -3
  501. nautobot/extras/tests/integration/test_computedfields.py +1 -1
  502. nautobot/extras/tests/integration/test_configcontextschema.py +7 -5
  503. nautobot/extras/tests/integration/test_customfields.py +4 -2
  504. nautobot/extras/tests/integration/test_dynamicgroups.py +2 -2
  505. nautobot/extras/tests/integration/test_jobs.py +25 -27
  506. nautobot/extras/tests/integration/test_notes.py +8 -4
  507. nautobot/extras/tests/integration/test_plugins.py +4 -4
  508. nautobot/extras/tests/integration/test_relationships.py +2 -2
  509. nautobot/extras/tests/test_api.py +371 -381
  510. nautobot/extras/tests/test_changelog.py +17 -16
  511. nautobot/extras/tests/test_context_managers.py +5 -6
  512. nautobot/extras/tests/test_customfields.py +112 -73
  513. nautobot/extras/tests/test_datasources.py +191 -117
  514. nautobot/extras/tests/test_dynamicgroups.py +45 -68
  515. nautobot/extras/tests/test_filters.py +170 -130
  516. nautobot/extras/tests/test_forms.py +107 -109
  517. nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
  518. nautobot/extras/tests/test_jobs.py +271 -273
  519. nautobot/extras/tests/test_management.py +3 -6
  520. nautobot/extras/tests/test_migrations.py +5 -3
  521. nautobot/extras/tests/test_models.py +121 -173
  522. nautobot/extras/tests/test_notes.py +0 -1
  523. nautobot/extras/tests/test_plugins.py +55 -89
  524. nautobot/extras/tests/test_relationships.py +174 -130
  525. nautobot/extras/tests/test_tags.py +6 -12
  526. nautobot/extras/tests/test_utils.py +31 -1
  527. nautobot/extras/tests/test_views.py +223 -184
  528. nautobot/extras/tests/test_webhooks.py +16 -15
  529. nautobot/extras/urls.py +69 -69
  530. nautobot/extras/utils.py +137 -163
  531. nautobot/extras/views.py +81 -153
  532. nautobot/ipam/api/fields.py +17 -0
  533. nautobot/ipam/api/serializers.py +77 -164
  534. nautobot/ipam/api/urls.py +4 -1
  535. nautobot/ipam/api/views.py +28 -19
  536. nautobot/ipam/apps.py +1 -0
  537. nautobot/ipam/choices.py +5 -12
  538. nautobot/ipam/constants.py +1 -0
  539. nautobot/ipam/factory.py +41 -30
  540. nautobot/ipam/filters.py +58 -25
  541. nautobot/ipam/forms.py +82 -211
  542. nautobot/ipam/graphql/types.py +0 -9
  543. nautobot/ipam/lookups.py +13 -8
  544. nautobot/ipam/management/commands/__init__.py +0 -0
  545. nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
  546. nautobot/ipam/migrations/0001_initial_part_1.py +0 -1
  547. nautobot/ipam/migrations/0002_initial_part_2.py +0 -1
  548. nautobot/ipam/migrations/0003_remove_max_length.py +0 -1
  549. nautobot/ipam/migrations/0004_fixup_p2p_broadcast.py +0 -1
  550. nautobot/ipam/migrations/0005_auto_slug.py +0 -1
  551. nautobot/ipam/migrations/0006_ipaddress_nat_outside_list.py +0 -1
  552. nautobot/ipam/migrations/0007_add_natural_indexing.py +0 -1
  553. nautobot/ipam/migrations/0008_prefix_vlan_vlangroup_location.py +0 -1
  554. nautobot/ipam/migrations/0009_alter_vlan_name.py +0 -1
  555. nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -2
  556. nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -39
  557. nautobot/ipam/migrations/0012_rename_ipam_roles.py +0 -1
  558. nautobot/ipam/migrations/0013_delete_role.py +0 -1
  559. nautobot/ipam/migrations/0014_rename_foreign_keys_and_related_names.py +0 -1
  560. nautobot/ipam/migrations/0015_prefix_add_type.py +0 -1
  561. nautobot/ipam/migrations/0016_prefix_type_data_migration.py +0 -3
  562. nautobot/ipam/migrations/0017_prefix_remove_is_pool.py +0 -1
  563. nautobot/ipam/migrations/0018_remove_site_foreign_key_from_ipam_models.py +0 -1
  564. nautobot/ipam/migrations/0019_created_datetime.py +0 -1
  565. nautobot/ipam/migrations/0020_related_name_changes.py +1 -2
  566. nautobot/ipam/migrations/0021_prefix_add_rir_and_date_allocated.py +0 -1
  567. nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +3 -5
  568. nautobot/ipam/migrations/0023_delete_aggregate.py +0 -1
  569. nautobot/ipam/migrations/0024_interface_to_ipaddress_m2m.py +0 -1
  570. nautobot/ipam/migrations/0025_interface_ipaddress_m2m_data_migration.py +0 -1
  571. nautobot/ipam/migrations/0026_ipaddress_remove_assigned_object.py +0 -1
  572. nautobot/ipam/migrations/0027_remove_rir_slug.py +16 -0
  573. nautobot/ipam/migrations/0028_tagsfield.py +44 -0
  574. nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
  575. nautobot/ipam/migrations/0030_ipam__namespaces.py +231 -0
  576. nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
  577. nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
  578. nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
  579. nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
  580. nautobot/ipam/models.py +579 -368
  581. nautobot/ipam/navigation.py +36 -159
  582. nautobot/ipam/querysets.py +117 -90
  583. nautobot/ipam/signals.py +89 -0
  584. nautobot/ipam/tables.py +86 -28
  585. nautobot/ipam/templates/ipam/ipaddress.html +14 -30
  586. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -0
  587. nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
  588. nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
  589. nautobot/ipam/templates/ipam/namespace_retrieve.html +42 -0
  590. nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
  591. nautobot/ipam/templates/ipam/prefix.html +27 -33
  592. nautobot/ipam/templates/ipam/prefix_edit.html +7 -1
  593. nautobot/ipam/templates/ipam/vlangroup.html +0 -13
  594. nautobot/ipam/templates/ipam/vrf.html +6 -4
  595. nautobot/ipam/templates/ipam/vrf_edit.html +20 -2
  596. nautobot/ipam/tests/integration/test_prefixes.py +4 -27
  597. nautobot/ipam/tests/test_api.py +60 -61
  598. nautobot/ipam/tests/test_filters.py +187 -126
  599. nautobot/ipam/tests/test_forms.py +12 -6
  600. nautobot/ipam/tests/test_graphql.py +8 -6
  601. nautobot/ipam/tests/test_migrations.py +8 -13
  602. nautobot/ipam/tests/test_models.py +426 -274
  603. nautobot/ipam/tests/test_ordering.py +6 -3
  604. nautobot/ipam/tests/test_querysets.py +340 -96
  605. nautobot/ipam/tests/test_views.py +100 -55
  606. nautobot/ipam/urls.py +28 -5
  607. nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
  608. nautobot/ipam/utils/migrations.py +713 -0
  609. nautobot/ipam/views.py +237 -122
  610. nautobot/project-static/docs/404.html +1399 -166
  611. nautobot/project-static/docs/additional-features/caching.html +1416 -320
  612. nautobot/project-static/docs/additional-features/change-logging.html +1389 -190
  613. nautobot/project-static/docs/additional-features/config-contexts.html +1389 -190
  614. nautobot/project-static/docs/additional-features/graphql.html +1389 -190
  615. nautobot/project-static/docs/additional-features/healthcheck.html +1389 -190
  616. nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1393 -190
  617. nautobot/project-static/docs/additional-features/jobs.html +1677 -460
  618. nautobot/project-static/docs/additional-features/napalm.html +1389 -190
  619. nautobot/project-static/docs/additional-features/prometheus-metrics.html +1389 -190
  620. nautobot/project-static/docs/additional-features/template-filters.html +1389 -190
  621. nautobot/project-static/docs/administration/celery-queues.html +1389 -190
  622. nautobot/project-static/docs/administration/nautobot-server.html +1553 -375
  623. nautobot/project-static/docs/administration/nautobot-shell.html +1395 -196
  624. nautobot/project-static/docs/administration/permissions.html +1389 -190
  625. nautobot/project-static/docs/administration/replicating-nautobot.html +1387 -207
  626. nautobot/project-static/docs/apps/index.html +1389 -190
  627. nautobot/project-static/docs/apps/nautobot-apps.html +1387 -175
  628. nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js +29 -0
  629. nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js.map +8 -0
  630. nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js → search.208ed371.min.js} +9 -15
  631. nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js.map → search.208ed371.min.js.map} +4 -4
  632. nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css +1 -0
  633. nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css.map +1 -0
  634. nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css +1 -0
  635. nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css.map +1 -0
  636. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1775 -590
  637. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1389 -190
  638. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3588 -1922
  639. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1461 -262
  640. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1401 -170
  641. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1396 -191
  642. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2095 -894
  643. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2357 -1194
  644. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2258 -940
  645. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1389 -190
  646. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1400 -201
  647. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11068 -7861
  648. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2867 -2224
  649. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1389 -190
  650. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2641 -1573
  651. nautobot/project-static/docs/configuration/authentication/ldap.html +1389 -190
  652. nautobot/project-static/docs/configuration/authentication/remote.html +1389 -190
  653. nautobot/project-static/docs/configuration/authentication/sso.html +1389 -190
  654. nautobot/project-static/docs/configuration/index.html +1398 -199
  655. nautobot/project-static/docs/configuration/optional-settings.html +1418 -274
  656. nautobot/project-static/docs/configuration/required-settings.html +1419 -287
  657. nautobot/project-static/docs/core-functionality/circuits.html +1446 -247
  658. nautobot/project-static/docs/core-functionality/device-types.html +1448 -249
  659. nautobot/project-static/docs/core-functionality/devices.html +1452 -249
  660. nautobot/project-static/docs/core-functionality/ipam.html +1452 -253
  661. nautobot/project-static/docs/core-functionality/power.html +1448 -249
  662. nautobot/project-static/docs/core-functionality/secrets.html +1448 -249
  663. nautobot/project-static/docs/core-functionality/services.html +1448 -249
  664. nautobot/project-static/docs/core-functionality/sites-and-racks.html +1448 -249
  665. nautobot/project-static/docs/core-functionality/tenancy.html +1448 -249
  666. nautobot/project-static/docs/core-functionality/virtualization.html +1452 -249
  667. nautobot/project-static/docs/core-functionality/vlans.html +1448 -249
  668. nautobot/project-static/docs/development/application-registry.html +1393 -214
  669. nautobot/project-static/docs/development/best-practices.html +1392 -192
  670. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1390 -191
  671. nautobot/project-static/docs/development/extending-models.html +1443 -257
  672. nautobot/project-static/docs/development/generic-views.html +1403 -174
  673. nautobot/project-static/docs/development/getting-started.html +1568 -262
  674. nautobot/project-static/docs/development/homepage.html +1389 -190
  675. nautobot/project-static/docs/development/index.html +1389 -190
  676. nautobot/project-static/docs/development/model-features.html +1389 -190
  677. nautobot/project-static/docs/development/natural-keys.html +1389 -190
  678. nautobot/project-static/docs/development/navigation-menu.html +1451 -330
  679. nautobot/project-static/docs/development/react-ui.html +4199 -0
  680. nautobot/project-static/docs/development/release-checklist.html +1392 -193
  681. nautobot/project-static/docs/development/role-internals.html +1402 -172
  682. nautobot/project-static/docs/development/style-guide.html +1399 -199
  683. nautobot/project-static/docs/development/templates.html +1391 -191
  684. nautobot/project-static/docs/development/testing.html +1389 -190
  685. nautobot/project-static/docs/development/user-preferences.html +1389 -190
  686. nautobot/project-static/docs/docker/index.html +1408 -206
  687. nautobot/project-static/docs/index.html +1397 -180
  688. nautobot/project-static/docs/installation/centos.html +1401 -170
  689. nautobot/project-static/docs/installation/external-authentication.html +1389 -190
  690. nautobot/project-static/docs/installation/http-server.html +1389 -190
  691. nautobot/project-static/docs/installation/index.html +1394 -191
  692. nautobot/project-static/docs/installation/migrating-from-netbox.html +1452 -305
  693. nautobot/project-static/docs/installation/migrating-from-postgresql.html +1390 -191
  694. nautobot/project-static/docs/installation/nautobot.html +1390 -191
  695. nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1389 -190
  696. nautobot/project-static/docs/installation/selinux-troubleshooting.html +1401 -170
  697. nautobot/project-static/docs/installation/services.html +1389 -190
  698. nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  699. nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  700. nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  701. nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  702. nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
  703. nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  704. nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  705. nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  706. nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  707. nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  708. nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  709. nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  710. nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  711. nautobot/project-static/docs/installation/ubuntu.html +1401 -170
  712. nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +4254 -1923
  713. nautobot/project-static/docs/installation/upgrading.html +1395 -192
  714. nautobot/project-static/docs/models/circuits/circuit.html +1427 -174
  715. nautobot/project-static/docs/models/circuits/circuittermination.html +1427 -174
  716. nautobot/project-static/docs/models/circuits/circuittype.html +1427 -174
  717. nautobot/project-static/docs/models/circuits/provider.html +1427 -174
  718. nautobot/project-static/docs/models/circuits/providernetwork.html +1427 -174
  719. nautobot/project-static/docs/models/dcim/cable.html +1458 -174
  720. nautobot/project-static/docs/models/dcim/consoleport.html +1427 -174
  721. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1427 -174
  722. nautobot/project-static/docs/models/dcim/consoleserverport.html +1427 -174
  723. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1427 -174
  724. nautobot/project-static/docs/models/dcim/device.html +1431 -174
  725. nautobot/project-static/docs/models/dcim/devicebay.html +1427 -174
  726. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1427 -174
  727. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1522 -177
  728. nautobot/project-static/docs/models/dcim/devicetype.html +1427 -174
  729. nautobot/project-static/docs/models/dcim/frontport.html +1427 -174
  730. nautobot/project-static/docs/models/dcim/frontporttemplate.html +1427 -174
  731. nautobot/project-static/docs/models/dcim/interface.html +1427 -174
  732. nautobot/project-static/docs/models/dcim/interfacetemplate.html +1427 -174
  733. nautobot/project-static/docs/models/dcim/inventoryitem.html +1427 -174
  734. nautobot/project-static/docs/models/dcim/location.html +1427 -174
  735. nautobot/project-static/docs/models/dcim/locationtype.html +1427 -174
  736. nautobot/project-static/docs/models/dcim/manufacturer.html +1427 -174
  737. nautobot/project-static/docs/models/dcim/platform.html +1427 -174
  738. nautobot/project-static/docs/models/dcim/powerfeed.html +1425 -172
  739. nautobot/project-static/docs/models/dcim/poweroutlet.html +1427 -174
  740. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1427 -174
  741. nautobot/project-static/docs/models/dcim/powerpanel.html +1425 -172
  742. nautobot/project-static/docs/models/dcim/powerport.html +1427 -174
  743. nautobot/project-static/docs/models/dcim/powerporttemplate.html +1427 -174
  744. nautobot/project-static/docs/models/dcim/rack.html +1427 -174
  745. nautobot/project-static/docs/models/dcim/rackgroup.html +1427 -174
  746. nautobot/project-static/docs/models/dcim/rackreservation.html +1427 -174
  747. nautobot/project-static/docs/models/dcim/rearport.html +1427 -174
  748. nautobot/project-static/docs/models/dcim/rearporttemplate.html +1427 -174
  749. nautobot/project-static/docs/models/dcim/region.html +1401 -170
  750. nautobot/project-static/docs/models/dcim/site.html +1401 -170
  751. nautobot/project-static/docs/models/dcim/virtualchassis.html +1425 -172
  752. nautobot/project-static/docs/models/extras/computedfield.html +1393 -194
  753. nautobot/project-static/docs/models/extras/configcontext.html +1465 -174
  754. nautobot/project-static/docs/models/extras/configcontextschema.html +1421 -168
  755. nautobot/project-static/docs/models/extras/customfield.html +1389 -190
  756. nautobot/project-static/docs/models/extras/customlink.html +1389 -190
  757. nautobot/project-static/docs/models/extras/dynamicgroup.html +1398 -199
  758. nautobot/project-static/docs/models/extras/exporttemplate.html +1389 -190
  759. nautobot/project-static/docs/models/extras/gitrepository.html +1393 -190
  760. nautobot/project-static/docs/models/extras/graphqlquery.html +1469 -171
  761. nautobot/project-static/docs/models/extras/imageattachment.html +1434 -181
  762. nautobot/project-static/docs/models/extras/job.html +1411 -157
  763. nautobot/project-static/docs/models/extras/jobbutton.html +1410 -207
  764. nautobot/project-static/docs/models/extras/jobhook.html +1397 -194
  765. nautobot/project-static/docs/models/extras/joblogentry.html +1408 -155
  766. nautobot/project-static/docs/models/extras/jobresult.html +1417 -159
  767. nautobot/project-static/docs/models/extras/note.html +1389 -190
  768. nautobot/project-static/docs/models/extras/relationship.html +1391 -192
  769. nautobot/project-static/docs/models/extras/role.html +1495 -198
  770. nautobot/project-static/docs/models/extras/secret.html +1492 -201
  771. nautobot/project-static/docs/models/extras/secretsgroup.html +1410 -157
  772. nautobot/project-static/docs/models/extras/status.html +1381 -221
  773. nautobot/project-static/docs/models/extras/tag.html +1389 -190
  774. nautobot/project-static/docs/models/extras/webhook.html +1389 -190
  775. nautobot/project-static/docs/models/ipam/ipaddress.html +1488 -200
  776. nautobot/project-static/docs/models/ipam/prefix.html +1410 -157
  777. nautobot/project-static/docs/models/ipam/rir.html +1410 -157
  778. nautobot/project-static/docs/models/ipam/routetarget.html +1410 -157
  779. nautobot/project-static/docs/models/ipam/service.html +1410 -157
  780. nautobot/project-static/docs/models/ipam/vlan.html +1410 -157
  781. nautobot/project-static/docs/models/ipam/vlangroup.html +1410 -157
  782. nautobot/project-static/docs/models/ipam/vrf.html +1410 -161
  783. nautobot/project-static/docs/models/tenancy/tenant.html +1412 -159
  784. nautobot/project-static/docs/models/tenancy/tenantgroup.html +1412 -159
  785. nautobot/project-static/docs/models/users/objectpermission.html +1462 -171
  786. nautobot/project-static/docs/models/users/token.html +1410 -157
  787. nautobot/project-static/docs/models/virtualization/cluster.html +1410 -157
  788. nautobot/project-static/docs/models/virtualization/clustergroup.html +1410 -157
  789. nautobot/project-static/docs/models/virtualization/clustertype.html +1410 -157
  790. nautobot/project-static/docs/models/virtualization/virtualmachine.html +1414 -157
  791. nautobot/project-static/docs/models/virtualization/vminterface.html +1410 -157
  792. nautobot/project-static/docs/objects.inv +0 -0
  793. nautobot/project-static/docs/plugins/development.html +1916 -646
  794. nautobot/project-static/docs/plugins/index.html +1389 -190
  795. nautobot/project-static/docs/plugins/porting-from-netbox.html +1389 -190
  796. nautobot/project-static/docs/release-notes/index.html +1389 -190
  797. nautobot/project-static/docs/release-notes/version-1.0.html +1389 -190
  798. nautobot/project-static/docs/release-notes/version-1.1.html +1389 -190
  799. nautobot/project-static/docs/release-notes/version-1.2.html +1389 -190
  800. nautobot/project-static/docs/release-notes/version-1.3.html +1389 -190
  801. nautobot/project-static/docs/release-notes/version-1.4.html +1389 -190
  802. nautobot/project-static/docs/release-notes/version-1.5.html +2016 -397
  803. nautobot/project-static/docs/release-notes/version-2.0.html +1935 -287
  804. nautobot/project-static/docs/requirements.txt +5 -4
  805. nautobot/project-static/docs/rest-api/authentication.html +1389 -190
  806. nautobot/project-static/docs/rest-api/filtering.html +1389 -190
  807. nautobot/project-static/docs/rest-api/overview.html +2002 -576
  808. nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
  809. nautobot/project-static/docs/search/search_index.json +1 -1
  810. nautobot/project-static/docs/sitemap.xml +197 -187
  811. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  812. nautobot/project-static/docs/user-guides/custom-fields.html +1390 -191
  813. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1392 -193
  814. nautobot/project-static/docs/user-guides/getting-started/index.html +1388 -189
  815. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1388 -189
  816. nautobot/project-static/docs/user-guides/getting-started/ipam.html +1386 -187
  817. nautobot/project-static/docs/user-guides/getting-started/platforms.html +1448 -249
  818. nautobot/project-static/docs/user-guides/getting-started/regions.html +1411 -212
  819. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1395 -196
  820. nautobot/project-static/docs/user-guides/getting-started/tenants.html +1448 -249
  821. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1448 -249
  822. nautobot/project-static/docs/user-guides/git-data-source.html +1405 -206
  823. nautobot/project-static/docs/user-guides/graphql.html +1402 -203
  824. nautobot/project-static/docs/user-guides/relationships.html +1448 -249
  825. nautobot/project-static/docs/user-guides/s3-django-storage.html +1448 -249
  826. nautobot/project-static/js/forms.js +16 -9
  827. nautobot/project-static/js/theme.js +5 -0
  828. nautobot/tenancy/api/serializers.py +4 -34
  829. nautobot/tenancy/api/urls.py +1 -1
  830. nautobot/tenancy/filters/__init__.py +9 -7
  831. nautobot/tenancy/filters/mixins.py +3 -2
  832. nautobot/tenancy/forms.py +3 -36
  833. nautobot/tenancy/migrations/0001_initial.py +0 -1
  834. nautobot/tenancy/migrations/0002_auto_slug.py +0 -1
  835. nautobot/tenancy/migrations/0003_mptt_to_tree_queries.py +0 -1
  836. nautobot/tenancy/migrations/0004_change_tree_manager_on_tree_models.py +0 -1
  837. nautobot/tenancy/migrations/0005_rename_foreign_keys_and_related_names.py +0 -1
  838. nautobot/tenancy/migrations/0006_created_datetime.py +0 -1
  839. nautobot/tenancy/migrations/0007_remove_tenant_tenantgroup_slug.py +20 -0
  840. nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
  841. nautobot/tenancy/models.py +0 -30
  842. nautobot/tenancy/navigation.py +6 -39
  843. nautobot/tenancy/tables.py +4 -4
  844. nautobot/tenancy/templates/tenancy/tenant.html +12 -12
  845. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -1
  846. nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
  847. nautobot/tenancy/tests/test_api.py +1 -12
  848. nautobot/tenancy/tests/test_filters.py +20 -12
  849. nautobot/tenancy/tests/test_views.py +11 -29
  850. nautobot/tenancy/urls.py +10 -10
  851. nautobot/tenancy/views.py +0 -3
  852. nautobot/ui/.eslintignore +6 -0
  853. nautobot/ui/.gitignore +10 -0
  854. nautobot/ui/.prettierignore +9 -0
  855. nautobot/ui/.prettierrc +4 -0
  856. nautobot/ui/README.md +33 -0
  857. nautobot/ui/app_imports.js.j2 +7 -0
  858. nautobot/ui/craco.config.js +46 -0
  859. nautobot/ui/jsconfig-base.json +11 -0
  860. nautobot/ui/jsconfig.json +5 -0
  861. nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
  862. nautobot/ui/package-lock.json +21451 -0
  863. nautobot/ui/package.json +70 -0
  864. nautobot/ui/public/index.html +47 -0
  865. nautobot/ui/public/logo192.png +0 -0
  866. nautobot/ui/public/logo512.png +0 -0
  867. nautobot/ui/public/manifest.json +25 -0
  868. nautobot/ui/public/nautobot_logo.svg +131 -0
  869. nautobot/ui/public/robots.txt +3 -0
  870. nautobot/ui/src/App.js +71 -0
  871. nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
  872. nautobot/ui/src/components/AppTab.js +40 -0
  873. nautobot/ui/src/components/Apps.js +60 -0
  874. nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
  875. nautobot/ui/src/components/HomePanel.js +58 -0
  876. nautobot/ui/src/components/JobHistoryTable.js +78 -0
  877. nautobot/ui/src/components/Layout.js +53 -0
  878. nautobot/ui/src/components/LoadingWidget.js +25 -0
  879. nautobot/ui/src/components/Navbar.js +116 -0
  880. nautobot/ui/src/components/NotificationPopover.js +27 -0
  881. nautobot/ui/src/components/ObjectListTable.js +209 -0
  882. nautobot/ui/src/components/ReferenceDataTag.js +35 -0
  883. nautobot/ui/src/components/RouterButton.js +10 -0
  884. nautobot/ui/src/components/RouterLink.js +10 -0
  885. nautobot/ui/src/components/SidebarNav.js +147 -0
  886. nautobot/ui/src/components/Table.js +48 -0
  887. nautobot/ui/src/components/TableItem.js +71 -0
  888. nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
  889. nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
  890. nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
  891. nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
  892. nautobot/ui/src/components/__tests__/Table.test.js +36 -0
  893. nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
  894. nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
  895. nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
  896. nautobot/ui/src/components/pagination.js +93 -0
  897. nautobot/ui/src/components/paginator.js +79 -0
  898. nautobot/ui/src/components/paginator_form.js +43 -0
  899. nautobot/ui/src/components/usePagination.js +57 -0
  900. nautobot/ui/src/constants/apiPath.js +10 -0
  901. nautobot/ui/src/constants/icons.js +15 -0
  902. nautobot/ui/src/constants/size.js +15 -0
  903. nautobot/ui/src/index.js +65 -0
  904. nautobot/ui/src/reportWebVitals.js +15 -0
  905. nautobot/ui/src/router.js +77 -0
  906. nautobot/ui/src/utils/api.js +131 -0
  907. nautobot/ui/src/utils/app-import.js +15 -0
  908. nautobot/ui/src/utils/color.js +15 -0
  909. nautobot/ui/src/utils/date.js +14 -0
  910. nautobot/ui/src/utils/index.js +15 -0
  911. nautobot/ui/src/utils/navigation.js +32 -0
  912. nautobot/ui/src/utils/session.js +64 -0
  913. nautobot/ui/src/utils/store.js +242 -0
  914. nautobot/ui/src/utils/string.js +6 -0
  915. nautobot/ui/src/utils/url.js +4 -0
  916. nautobot/ui/src/views/Home.js +138 -0
  917. nautobot/ui/src/views/InstalledApps.js +80 -0
  918. nautobot/ui/src/views/Login.js +48 -0
  919. nautobot/ui/src/views/Logout.js +20 -0
  920. nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
  921. nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
  922. nautobot/ui/src/views/__tests__/Login.test.js +15 -0
  923. nautobot/ui/src/views/generic/GenericView.js +142 -0
  924. nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
  925. nautobot/ui/src/views/generic/ObjectList.js +127 -0
  926. nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
  927. nautobot/users/admin.py +1 -1
  928. nautobot/users/api/serializers.py +51 -61
  929. nautobot/users/api/urls.py +1 -1
  930. nautobot/users/api/views.py +53 -2
  931. nautobot/users/migrations/0001_initial.py +0 -1
  932. nautobot/users/migrations/0002_token_ordering_by_created.py +0 -1
  933. nautobot/users/migrations/0003_alter_user_options.py +0 -1
  934. nautobot/users/migrations/0004_alter_user_managers.py +0 -1
  935. nautobot/users/tests/test_api.py +109 -28
  936. nautobot/users/tests/test_filters.py +0 -4
  937. nautobot/users/tests/test_models.py +0 -1
  938. nautobot/users/views.py +0 -7
  939. nautobot/virtualization/api/serializers.py +18 -132
  940. nautobot/virtualization/api/urls.py +1 -1
  941. nautobot/virtualization/api/views.py +1 -22
  942. nautobot/virtualization/choices.py +0 -2
  943. nautobot/virtualization/filters.py +12 -7
  944. nautobot/virtualization/forms.py +21 -117
  945. nautobot/virtualization/migrations/0001_initial.py +0 -1
  946. nautobot/virtualization/migrations/0002_virtualmachine_local_context_schema.py +0 -1
  947. nautobot/virtualization/migrations/0003_vminterface_verbose_name.py +0 -1
  948. nautobot/virtualization/migrations/0004_auto_slug.py +0 -1
  949. nautobot/virtualization/migrations/0005_add_natural_indexing.py +0 -1
  950. nautobot/virtualization/migrations/0006_vminterface_status.py +0 -1
  951. nautobot/virtualization/migrations/0007_vminterface_status_data_migration.py +0 -1
  952. nautobot/virtualization/migrations/0008_vminterface_parent.py +0 -1
  953. nautobot/virtualization/migrations/0009_cluster_location.py +0 -1
  954. nautobot/virtualization/migrations/0010_vminterface_mac_address_data_migration.py +0 -1
  955. nautobot/virtualization/migrations/0011_alter_vminterface_mac_address.py +0 -1
  956. nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -2
  957. nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -12
  958. nautobot/virtualization/migrations/0014_rename_virtualmachine_roles.py +0 -1
  959. nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -2
  960. nautobot/virtualization/migrations/0016_remove_site_foreign_key_from_cluster_class.py +0 -1
  961. nautobot/virtualization/migrations/0017_created_datetime.py +0 -1
  962. nautobot/virtualization/migrations/0018_related_name_changes.py +1 -2
  963. nautobot/virtualization/migrations/0019_vminterface_ip_addresses_m2m.py +0 -1
  964. nautobot/virtualization/migrations/0020_remove_clustergroup_clustertype_slug.py +20 -0
  965. nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
  966. nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
  967. nautobot/virtualization/migrations/0023_ipam__namespaces.py +25 -0
  968. nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
  969. nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
  970. nautobot/virtualization/models.py +39 -131
  971. nautobot/virtualization/navigation.py +18 -99
  972. nautobot/virtualization/tables.py +4 -4
  973. nautobot/virtualization/templates/virtualization/virtualmachine.html +13 -2
  974. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
  975. nautobot/virtualization/tests/test_api.py +42 -52
  976. nautobot/virtualization/tests/test_filters.py +98 -75
  977. nautobot/virtualization/tests/test_models.py +36 -13
  978. nautobot/virtualization/tests/test_views.py +68 -73
  979. nautobot/virtualization/urls.py +10 -10
  980. nautobot/virtualization/views.py +8 -14
  981. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +15 -22
  982. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +987 -834
  983. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
  984. nautobot/circuits/api/nested_serializers.py +0 -69
  985. nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
  986. nautobot/dcim/api/nested_serializers.py +0 -356
  987. nautobot/dcim/templates/dcim/device_import.html +0 -5
  988. nautobot/dcim/templates/dcim/device_import_child.html +0 -5
  989. nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
  990. nautobot/extras/api/nested_serializers.py +0 -353
  991. nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -42
  992. nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -47
  993. nautobot/extras/reports.py +0 -60
  994. nautobot/extras/scripts.py +0 -72
  995. nautobot/extras/tests/example_jobs/script_variables.py +0 -67
  996. nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
  997. nautobot/extras/tests/example_jobs/test_fail.py +0 -16
  998. nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
  999. nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
  1000. nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
  1001. nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
  1002. nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
  1003. nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
  1004. nautobot/extras/tests/example_jobs/test_modify_db.py +0 -19
  1005. nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
  1006. nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
  1007. nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
  1008. nautobot/extras/tests/example_jobs/test_pass.py +0 -19
  1009. nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
  1010. nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
  1011. nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
  1012. nautobot/ipam/api/nested_serializers.py +0 -143
  1013. nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js +0 -29
  1014. nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js.map +0 -8
  1015. nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js +0 -18
  1016. nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js.map +0 -8
  1017. nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css +0 -1
  1018. nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css.map +0 -1
  1019. nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css +0 -1
  1020. nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css.map +0 -1
  1021. nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css +0 -1
  1022. nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css.map +0 -1
  1023. nautobot/tenancy/api/nested_serializers.py +0 -31
  1024. nautobot/users/api/nested_serializers.py +0 -67
  1025. nautobot/virtualization/api/nested_serializers.py +0 -65
  1026. /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
  1027. /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
  1028. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
  1029. {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 F, Q
10
- from django.urls import reverse
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 Device, Interface
19
- from nautobot.extras.models import RoleModelMixin, Status, StatusModel
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 VirtualMachine, VMInterface
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.rd:
118
- return f"{self.name} ({self.rd})"
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, StatusModel, RoleModelMixin):
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
- vrf = models.ForeignKey(
251
- to="ipam.VRF",
440
+ namespace = models.ForeignKey(
441
+ to="ipam.Namespace",
252
442
  on_delete=models.PROTECT,
253
443
  related_name="prefixes",
254
- blank=True,
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
- F("vrf__name").asc(nulls_first=True),
499
+ "namespace",
500
+ "ip_version",
334
501
  "network",
335
502
  "prefix_length",
336
- ) # (vrf, prefix) may be non-unique
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(Prefix, self).__init__(*args, **kwargs)
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, pre):
348
- if pre:
349
- if isinstance(pre, str):
350
- pre = netaddr.IPNetwork(pre)
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 = pre.broadcast if pre.broadcast else pre[-1]
360
- self.network = str(pre.network)
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 = pre.prefixlen
544
+ self.prefix_length = prefix.prefixlen
545
+ self.ip_version = prefix.version
363
546
 
364
- def get_absolute_url(self):
365
- return reverse("ipam:prefix", args=[self.pk])
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 save(self, *args, **kwargs):
561
+ def delete(self, *args, **kwargs):
562
+ """
563
+ A Prefix with children will be impossible to delete and raise a `ProtectedError`.
392
564
 
393
- if isinstance(self.prefix, netaddr.IPNetwork):
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
- def to_csv(self):
401
- return (
402
- self.prefix,
403
- self.get_type_display(),
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
- @property
433
- def family(self):
434
- if self.prefix:
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
- def get_duplicates(self):
439
- return Prefix.objects.net_equals(self.prefix).filter(vrf=self.vrf).exclude(pk=self.pk)
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 get_child_prefixes(self):
656
+ def supernets(self, direct=False, include_self=False, for_update=False):
442
657
  """
443
- Return all Prefixes within this Prefix and VRF. If this Prefix is a container in the global table, return child
444
- Prefixes belonging to any VRF.
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
- if self.vrf is None and self.type == choices.PrefixTypeChoices.TYPE_CONTAINER:
447
- return Prefix.objects.net_contained(self.prefix)
448
- else:
449
- return Prefix.objects.net_contained(self.prefix).filter(vrf=self.vrf)
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 get_child_ips(self):
687
+ def subnets(self, direct=False, include_self=False, for_update=False):
452
688
  """
453
- Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return
454
- child IPAddresses belonging to any VRF.
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
- if self.vrf is None and self.type == choices.PrefixTypeChoices.TYPE_CONTAINER:
457
- return IPAddress.objects.net_host_contained(self.prefix)
458
- else:
459
- return IPAddress.objects.net_host_contained(self.prefix).filter(vrf=self.vrf)
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.get_child_prefixes()])
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.get_child_ips()])
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.family == 6,
803
+ self.ip_version == 6,
483
804
  self.type == choices.PrefixTypeChoices.TYPE_POOL,
484
- self.family == 4 and self.prefix.prefixlen >= 31,
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.prefix.prefixlen}"
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
- queryset = Prefix.objects.net_contained(self.prefix).filter(vrf=self.vrf)
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.prefix.version == 4,
535
- self.prefix.prefixlen < 31,
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, StatusModel, RoleModelMixin):
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
- broadcast = VarbinaryIPField(null=False, db_index=True, help_text="IPv4 or IPv6 broadcast address")
571
- prefix_length = models.IntegerField(null=False, db_index=True, help_text="Length of the Network prefix, in bits.")
572
- vrf = models.ForeignKey(
573
- to="ipam.VRF",
574
- on_delete=models.PROTECT,
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
- verbose_name="VRF",
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", "prefix_length") # address may be non-unique
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
- super(IPAddress, self).__init__(*args, **kwargs)
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.broadcast = str(broadcast)
655
- self.prefix_length = address.prefixlen
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
- # However, by default all BaseModel subclasses now have a `natural_key` property;
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(slug="slaac")
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
- if self.address:
688
-
689
- # /0 masks are not acceptable
690
- if self.address.prefixlen == 0:
691
- raise ValidationError({"address": "Cannot create IP address with /0 mask."})
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.family != 6:
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 to_csv(self):
731
-
732
- # Determine if this IP is primary for a Device
733
- is_primary = False
734
- if self.address.version == 4 and getattr(self, "primary_ip4_for", False):
735
- is_primary = True
736
- elif self.address.version == 6 and getattr(self, "primary_ip6_for", False):
737
- is_primary = True
738
-
739
- return (
740
- self.address,
741
- self.vrf.name if self.vrf else None,
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.prefix_length is not None:
753
- cidr = f"{self.host}/{self.prefix_length}"
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
- @property
762
- def family(self):
763
- if self.address:
764
- return self.address.version
765
- return None
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
- def validate_unique(self, exclude=None):
823
- """
824
- Check uniqueness on combination of `ip_address`, `interface` and `vm_interface` fields
825
- and raise ValidationError if check failed.
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, StatusModel, RoleModelMixin):
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)