nautobot 2.4.4__py3-none-any.whl → 2.4.6__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 (139) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/core/api/mixins.py +10 -0
  3. nautobot/core/celery/__init__.py +5 -3
  4. nautobot/core/celery/encoders.py +2 -2
  5. nautobot/core/forms/fields.py +21 -5
  6. nautobot/core/forms/utils.py +1 -0
  7. nautobot/core/jobs/__init__.py +3 -2
  8. nautobot/core/jobs/bulk_actions.py +1 -1
  9. nautobot/core/management/commands/generate_test_data.py +1 -1
  10. nautobot/core/models/name_color_content_types.py +9 -0
  11. nautobot/core/models/validators.py +7 -0
  12. nautobot/core/settings.py +0 -14
  13. nautobot/core/settings.yaml +0 -28
  14. nautobot/core/tables.py +6 -1
  15. nautobot/core/templates/generic/object_retrieve.html +1 -1
  16. nautobot/core/testing/__init__.py +2 -0
  17. nautobot/core/testing/api.py +18 -0
  18. nautobot/core/testing/mixins.py +9 -0
  19. nautobot/core/tests/nautobot_config.py +0 -2
  20. nautobot/core/tests/runner.py +17 -140
  21. nautobot/core/tests/test_api.py +4 -4
  22. nautobot/core/tests/test_authentication.py +83 -4
  23. nautobot/core/tests/test_forms.py +11 -8
  24. nautobot/core/tests/test_graphql.py +9 -0
  25. nautobot/core/tests/test_jobs.py +33 -27
  26. nautobot/core/ui/object_detail.py +31 -0
  27. nautobot/dcim/factory.py +2 -0
  28. nautobot/dcim/filters/__init__.py +5 -0
  29. nautobot/dcim/forms.py +17 -1
  30. nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
  31. nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
  32. nautobot/dcim/models/devices.py +9 -2
  33. nautobot/dcim/tables/devices.py +1 -0
  34. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
  35. nautobot/dcim/tests/test_api.py +74 -31
  36. nautobot/dcim/tests/test_filters.py +2 -0
  37. nautobot/dcim/tests/test_jobs.py +4 -6
  38. nautobot/dcim/tests/test_models.py +65 -0
  39. nautobot/dcim/tests/test_views.py +3 -0
  40. nautobot/extras/choices.py +8 -3
  41. nautobot/extras/forms/forms.py +7 -3
  42. nautobot/extras/jobs.py +181 -103
  43. nautobot/extras/management/utils.py +13 -2
  44. nautobot/extras/models/datasources.py +4 -1
  45. nautobot/extras/models/jobs.py +20 -17
  46. nautobot/extras/plugins/marketplace_manifest.yml +18 -0
  47. nautobot/extras/tables.py +29 -34
  48. nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
  49. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  50. nautobot/extras/templates/extras/status.html +1 -37
  51. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  52. nautobot/extras/test_jobs/fail.py +75 -1
  53. nautobot/extras/tests/integration/test_notes.py +1 -1
  54. nautobot/extras/tests/test_api.py +23 -8
  55. nautobot/extras/tests/test_changelog.py +4 -4
  56. nautobot/extras/tests/test_customfields.py +3 -0
  57. nautobot/extras/tests/test_datasources.py +64 -54
  58. nautobot/extras/tests/test_jobs.py +69 -62
  59. nautobot/extras/tests/test_models.py +1 -1
  60. nautobot/extras/tests/test_plugins.py +19 -13
  61. nautobot/extras/tests/test_relationships.py +14 -5
  62. nautobot/extras/tests/test_tags.py +2 -2
  63. nautobot/extras/tests/test_views.py +15 -6
  64. nautobot/extras/urls.py +1 -30
  65. nautobot/extras/views.py +17 -55
  66. nautobot/ipam/forms.py +15 -0
  67. nautobot/ipam/querysets.py +6 -0
  68. nautobot/ipam/tables.py +6 -2
  69. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
  70. nautobot/ipam/templates/ipam/rir.html +1 -43
  71. nautobot/ipam/templates/ipam/service.html +2 -46
  72. nautobot/ipam/templates/ipam/service_edit.html +1 -17
  73. nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
  74. nautobot/ipam/tests/migration/__init__.py +0 -0
  75. nautobot/ipam/tests/migration/test_migrations.py +510 -0
  76. nautobot/ipam/tests/test_api.py +66 -36
  77. nautobot/ipam/tests/test_filters.py +0 -10
  78. nautobot/ipam/tests/test_models.py +16 -0
  79. nautobot/ipam/tests/test_views.py +44 -2
  80. nautobot/ipam/urls.py +2 -67
  81. nautobot/ipam/utils/migrations.py +185 -152
  82. nautobot/ipam/utils/testing.py +177 -0
  83. nautobot/ipam/views.py +119 -198
  84. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  85. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
  86. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
  87. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  88. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
  89. nautobot/project-static/docs/development/apps/api/testing.html +0 -87
  90. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  91. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  92. nautobot/project-static/docs/development/core/getting-started.html +78 -107
  93. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  94. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  95. nautobot/project-static/docs/development/core/testing.html +24 -198
  96. nautobot/project-static/docs/development/jobs/index.html +27 -14
  97. nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
  98. nautobot/project-static/docs/objects.inv +0 -0
  99. nautobot/project-static/docs/overview/application_stack.html +1 -1
  100. nautobot/project-static/docs/release-notes/version-2.4.html +409 -1
  101. nautobot/project-static/docs/requirements.txt +1 -1
  102. nautobot/project-static/docs/search/search_index.json +1 -1
  103. nautobot/project-static/docs/sitemap.xml +290 -290
  104. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  105. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
  106. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
  107. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
  108. nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
  109. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  110. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
  111. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
  112. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
  113. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
  114. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
  115. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
  116. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  117. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  118. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
  119. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  120. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
  121. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  122. nautobot/project-static/docs/user-guide/index.html +89 -2
  123. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
  124. nautobot/virtualization/forms.py +20 -0
  125. nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
  126. nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
  127. nautobot/virtualization/tests/test_api.py +14 -3
  128. nautobot/virtualization/tests/test_views.py +10 -2
  129. nautobot/virtualization/urls.py +10 -93
  130. nautobot/virtualization/views.py +33 -72
  131. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/METADATA +8 -7
  132. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/RECORD +137 -132
  133. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
  134. nautobot/core/tests/performance_baselines.yml +0 -8900
  135. nautobot/ipam/tests/test_migrations.py +0 -462
  136. /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
  137. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
  138. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
  139. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,177 @@
1
+ """Utilities for testing IPAM functionality, including data migrations."""
2
+
3
+ import random
4
+
5
+ from django.apps import apps
6
+ from netaddr import IPNetwork
7
+
8
+ # Calculate the probabilities to use for the maybe_subdivide() function defined below.
9
+
10
+ # Frequency of IPv4 (leaf, network) Prefixes by each given mask length in a "realistic" data set.
11
+ # Based loosely on a survey of one large real-world deployment's IP space usage
12
+ FREQUENCY_BY_MASK_LENGTH = [
13
+ 0, # /0
14
+ 0,
15
+ 0,
16
+ 0,
17
+ 0, # /4
18
+ 0,
19
+ 0,
20
+ 0,
21
+ 0, # /8
22
+ 0,
23
+ 0,
24
+ 0,
25
+ 0, # /12
26
+ 1,
27
+ 2,
28
+ 4,
29
+ 16, # /16
30
+ 8,
31
+ 12,
32
+ 16,
33
+ 20, # /20
34
+ 120,
35
+ 150,
36
+ 400,
37
+ 5000, # /24
38
+ 13000,
39
+ 16000,
40
+ 19000,
41
+ 17000, # /28
42
+ 16000,
43
+ 32000,
44
+ 4000,
45
+ 1200, # /32
46
+ ]
47
+
48
+ # Amount of IP space needed at each mask length (frequency at this length, plus the rollup of subdivided nets)
49
+ # Start calculation from the /32 frequency:
50
+ CUMULATIVE_BY_MASK_LENGTH = [FREQUENCY_BY_MASK_LENGTH[-1]]
51
+ # Then work backwards to each parent mask length
52
+ for mask_len in range(len(FREQUENCY_BY_MASK_LENGTH) - 2, -1, -1): # 31, ... 0
53
+ CUMULATIVE_BY_MASK_LENGTH.append(CUMULATIVE_BY_MASK_LENGTH[-1] // 2 + FREQUENCY_BY_MASK_LENGTH[mask_len])
54
+ # Reverse the list to get order by ascending prefix length, same as FREQUENCY_BY_MASK_LENGTH
55
+ CUMULATIVE_BY_MASK_LENGTH.reverse()
56
+
57
+ # Chance to stop subdividing at any given prefix length and create a network Prefix with this length
58
+ CHANCE_TO_STOP = [
59
+ 0 if not cumulative else frequency / cumulative
60
+ for frequency, cumulative in zip(FREQUENCY_BY_MASK_LENGTH, CUMULATIVE_BY_MASK_LENGTH)
61
+ ]
62
+
63
+
64
+ def maybe_subdivide(network):
65
+ """
66
+ Generator for recursively and probabilistically subdividing a network into subnets.
67
+
68
+ Yields:
69
+ IPNetwork: each constructed subdivision of the given network
70
+ """
71
+ if random.random() < CHANCE_TO_STOP[network.prefixlen]: # noqa: S311 # suspicious-non-cryptographic-random-usage
72
+ # Do not subdivide any further
73
+ yield network
74
+ else:
75
+ # Split it into its two child networks and recurse
76
+ subnets = network.subnet(network.prefixlen + 1)
77
+ for subnet in subnets:
78
+ yield from maybe_subdivide(subnet)
79
+
80
+
81
+ def maybe_random_instance(queryset, chance_of_none=0.75):
82
+ """
83
+ Helper function - randomly return either a random instance of the given queryset or None.
84
+ """
85
+ if random.random() < chance_of_none: # noqa: S311 # suspicious-non-cryptographic-random-usage
86
+ return None
87
+ return random.choice(queryset) # noqa: S311 # suspicious-non-cryptographic-random-usage
88
+
89
+
90
+ def create_prefixes_and_ips(initial_subnet: str, apps=apps, seed="Nautobot"): # pylint: disable=redefined-outer-name
91
+ """
92
+ Create many (Nautobot 1.x) Prefix and IPAddress records under a given initial_subnet.
93
+
94
+ The specific records created are pseudo-random (consistent for any given `initial_subnet` and `seed` values),
95
+ but will *in general* consist of about 95% coverage of the subnet by non-overlapping Prefix partitions and about
96
+ 5% coverage of the subnet by individual IPAddress records. Additionally, about 10% of Prefixes and IPAddresses
97
+ respectively will be duplicated one or more times.
98
+
99
+ Args:
100
+ initial_subnet (str): The parent subnet ("10.0.0.0/16") that will encompass the Prefix and IPAddress records.
101
+ apps: Django application registry containing definitions of the (historical) Prefix, IPAddress, etc. models.
102
+ seed: Random generator seed to ensure reproducible pseudo-random construction of the data.
103
+ """
104
+ IPAddress = apps.get_model("ipam", "IPAddress")
105
+ Prefix = apps.get_model("ipam", "Prefix")
106
+ Status = apps.get_model("extras", "Status")
107
+ Tenant = apps.get_model("tenancy", "Tenant")
108
+ VRF = apps.get_model("ipam", "VRF")
109
+
110
+ print(f"Seeding the PRNG with seed {seed}")
111
+ random.seed(seed) # suspicious-non-cryptographic-random-usage
112
+
113
+ status_active, _ = Status.objects.get_or_create(name="Active", defaults={"slug": "active"})
114
+
115
+ for i in range(1, 11):
116
+ Tenant.objects.create(name=f"{initial_subnet} Tenant {i}")
117
+ VRF.objects.create(name=f"{initial_subnet} VRF {i}", enforce_unique=False) # TODO should some enforce_unique?
118
+
119
+ all_tenants = list(Tenant.objects.all())
120
+ all_vrfs = list(VRF.objects.all())
121
+
122
+ print(f"Creating Prefixes to subdivide {initial_subnet}")
123
+ unique_prefix_count = 0
124
+ duplicate_prefix_count = 0
125
+ for subnet in maybe_subdivide(IPNetwork(initial_subnet)):
126
+ if random.random() < 0.95: # noqa: S311 # suspicious-non-cryptographic-random-usage
127
+ # 95% chance to create any given Prefix
128
+ Prefix.objects.create(
129
+ network=str(subnet.network),
130
+ broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
131
+ prefix_length=subnet.prefixlen,
132
+ status=status_active,
133
+ tenant=maybe_random_instance(all_tenants),
134
+ vrf=maybe_random_instance(all_vrfs),
135
+ )
136
+ unique_prefix_count += 1
137
+ while random.random() < 0.1: # noqa: S311 # suspicious-non-cryptographic-random-usage
138
+ # 10% repeating chance to create a duplicate(s) of this Prefix
139
+ Prefix.objects.create(
140
+ network=str(subnet.network),
141
+ broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
142
+ prefix_length=subnet.prefixlen,
143
+ status=status_active,
144
+ tenant=maybe_random_instance(all_tenants),
145
+ vrf=maybe_random_instance(all_vrfs),
146
+ )
147
+ duplicate_prefix_count += 1
148
+ print(f"Created {unique_prefix_count} unique Prefixes and {duplicate_prefix_count} duplicates")
149
+
150
+ print(f"Creating IPAddresses within {initial_subnet}")
151
+ unique_ip_count = 0
152
+ duplicate_ip_count = 0
153
+ for ip in IPNetwork(initial_subnet):
154
+ if random.random() < 0.05: # noqa: S311 # suspicious-non-cryptographic-random-usage
155
+ # 5% chance to create any given IP address
156
+ network = IPNetwork(ip)
157
+ IPAddress.objects.create(
158
+ host=str(network.ip),
159
+ broadcast=str(network.broadcast if network.broadcast else network[-1]),
160
+ prefix_length=network.prefixlen,
161
+ status=status_active,
162
+ tenant=maybe_random_instance(all_tenants),
163
+ vrf=maybe_random_instance(all_vrfs),
164
+ )
165
+ unique_ip_count += 1
166
+ while random.random() < 0.1: # noqa: S311 # suspicious-non-cryptographic-random-usage
167
+ # 10% repeating chance to create a duplicate(s) of this IP
168
+ IPAddress.objects.create(
169
+ host=str(network.ip),
170
+ broadcast=str(network.broadcast if network.broadcast else network[-1]),
171
+ prefix_length=network.prefixlen,
172
+ status=status_active,
173
+ tenant=maybe_random_instance(all_tenants),
174
+ vrf=maybe_random_instance(all_vrfs),
175
+ )
176
+ duplicate_ip_count += 1
177
+ print(f"Created {unique_ip_count} unique IPAddresses and {duplicate_ip_count} duplicates")
nautobot/ipam/views.py CHANGED
@@ -16,6 +16,8 @@ from django.utils.http import urlencode
16
16
  from django.views.generic import View
17
17
  from django_tables2 import RequestConfig
18
18
  import netaddr
19
+ from rest_framework.decorators import action
20
+ from rest_framework.response import Response
19
21
 
20
22
  from nautobot.cloud.tables import CloudNetworkTable
21
23
  from nautobot.core.constants import MAX_PAGE_SIZE_DEFAULT
@@ -63,27 +65,7 @@ logger = logging.getLogger(__name__)
63
65
  #
64
66
 
65
67
 
66
- def get_namespace_related_counts(instance, request):
67
- """Return counts of all IPAM objects related to the given Namespace."""
68
- return {
69
- "vrf_count": instance.vrfs.restrict(request.user, "view").count(),
70
- "prefix_count": instance.prefixes.restrict(request.user, "view").count(),
71
- "ip_address_count": instance.ip_addresses.restrict(request.user, "view").count(),
72
- }
73
-
74
-
75
- class NamespaceUIViewSet(
76
- view_mixins.ObjectDetailViewMixin,
77
- view_mixins.ObjectListViewMixin,
78
- view_mixins.ObjectEditViewMixin,
79
- view_mixins.ObjectDestroyViewMixin,
80
- view_mixins.ObjectChangeLogViewMixin,
81
- view_mixins.ObjectBulkCreateViewMixin, # 3.0 TODO: remove, no longer used
82
- view_mixins.ObjectBulkDestroyViewMixin,
83
- view_mixins.ObjectBulkUpdateViewMixin,
84
- view_mixins.ObjectNotesViewMixin,
85
- ):
86
- lookup_field = "pk"
68
+ class NamespaceUIViewSet(NautobotUIViewSet):
87
69
  form_class = forms.NamespaceForm
88
70
  bulk_update_form_class = forms.NamespaceBulkEditForm
89
71
  filterset_class = filters.NamespaceFilterSet
@@ -91,134 +73,95 @@ class NamespaceUIViewSet(
91
73
  queryset = Namespace.objects.all()
92
74
  serializer_class = serializers.NamespaceSerializer
93
75
  table_class = tables.NamespaceTable
76
+ object_detail_content = object_detail.ObjectDetailContent(
77
+ panels=(object_detail.ObjectFieldsPanel(section=SectionChoices.LEFT_HALF, weight=100, fields="__all__"),),
78
+ extra_tabs=(
79
+ object_detail.DistinctViewTab(
80
+ weight=800,
81
+ tab_id="vrfs",
82
+ label="VRFs",
83
+ url_name="ipam:namespace_vrfs",
84
+ related_object_attribute="vrfs",
85
+ ),
86
+ object_detail.DistinctViewTab(
87
+ weight=900,
88
+ tab_id="prefixes",
89
+ label="Prefixes",
90
+ url_name="ipam:namespace_prefixes",
91
+ related_object_attribute="prefixes",
92
+ ),
93
+ object_detail.DistinctViewTab(
94
+ weight=1000,
95
+ tab_id="ip_addresses",
96
+ label="IP Addresses",
97
+ url_name="ipam:namespace_ip_addresses",
98
+ related_object_attribute="ip_addresses",
99
+ ),
100
+ ),
101
+ )
94
102
 
95
103
  def get_extra_context(self, request, instance):
96
104
  context = super().get_extra_context(request, instance)
97
-
98
- if self.action == "retrieve":
99
- context.update(get_namespace_related_counts(instance, request))
100
-
105
+ context.update({"object_detail_content": self.object_detail_content})
101
106
  return context
102
107
 
108
+ @action(detail=True, url_path="vrfs")
109
+ def vrfs(self, request, *args, **kwargs):
110
+ instance = self.get_object()
111
+ vrfs = instance.vrfs.restrict(request.user, "view")
112
+ vrf_table = tables.VRFTable(
113
+ data=vrfs,
114
+ user=request.user,
115
+ exclude=["namespace"],
116
+ )
117
+ if request.user.has_perm("ipam.change_vrf") or request.user.has_perm("ipam.delete_vrf"):
118
+ vrf_table.columns.show("pk")
103
119
 
104
- class NamespaceIPAddressesView(generic.ObjectView):
105
- queryset = Namespace.objects.all()
106
- template_name = "ipam/namespace_ipaddresses.html"
107
-
108
- def get_extra_context(self, request, instance):
109
- # Find all IPAddresses belonging to this Namespace
110
- ip_addresses = instance.ip_addresses.restrict(request.user, "view").select_related("role", "status", "tenant")
111
-
112
- ip_address_table = tables.IPAddressTable(ip_addresses, exclude=["namespace"])
113
- if request.user.has_perm("ipam.change_ipaddress") or request.user.has_perm("ipam.delete_ipaddress"):
114
- ip_address_table.columns.show("pk")
115
-
116
- paginate = {
117
- "paginator_class": EnhancedPaginator,
118
- "per_page": get_paginate_count(request),
119
- }
120
- RequestConfig(request, paginate).configure(ip_address_table)
121
-
122
- # Compile permissions list for rendering the object table
123
- permissions = {
124
- "add": request.user.has_perm("ipam.add_ipaddress"),
125
- "change": request.user.has_perm("ipam.change_ipaddress"),
126
- "delete": request.user.has_perm("ipam.delete_ipaddress"),
127
- }
128
- bulk_querystring = f"namespace={instance.id}"
129
-
130
- context = super().get_extra_context(request, instance)
131
- context.update(
120
+ RequestConfig(
121
+ request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
122
+ ).configure(vrf_table)
123
+ return Response(
132
124
  {
133
- "ip_address_table": ip_address_table,
134
- "permissions": permissions,
135
- "bulk_querystring": bulk_querystring,
136
- "active_tab": "ip-addresses",
125
+ "vrf_table": vrf_table,
126
+ "active_tab": "vrfs",
137
127
  }
138
128
  )
139
- context.update(get_namespace_related_counts(instance, request))
140
129
 
141
- return context
142
-
143
-
144
- class NamespacePrefixesView(generic.ObjectView):
145
- queryset = Namespace.objects.all()
146
- template_name = "ipam/namespace_prefixes.html"
147
-
148
- def get_extra_context(self, request, instance):
149
- # Find all Prefixes belonging to this Namespace
130
+ @action(detail=True, url_path="prefixes")
131
+ def prefixes(self, request, *args, **kwargs):
132
+ instance = self.get_object()
150
133
  prefixes = instance.prefixes.restrict(request.user, "view").select_related("status")
151
-
152
- prefix_table = tables.PrefixTable(prefixes, exclude=["namespace"])
134
+ prefix_table = tables.PrefixTable(data=prefixes, user=request.user, exclude=["namespace"])
153
135
  if request.user.has_perm("ipam.change_prefix") or request.user.has_perm("ipam.delete_prefix"):
154
136
  prefix_table.columns.show("pk")
155
137
 
156
- paginate = {
157
- "paginator_class": EnhancedPaginator,
158
- "per_page": get_paginate_count(request),
159
- }
160
- RequestConfig(request, paginate).configure(prefix_table)
161
-
162
- # Compile permissions list for rendering the object table
163
- permissions = {
164
- "add": request.user.has_perm("ipam.add_prefix"),
165
- "change": request.user.has_perm("ipam.change_prefix"),
166
- "delete": request.user.has_perm("ipam.delete_prefix"),
167
- }
168
- bulk_querystring = f"namespace={instance.id}"
169
-
170
- context = super().get_extra_context(request, instance)
171
- context.update(
138
+ RequestConfig(
139
+ request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
140
+ ).configure(prefix_table)
141
+ return Response(
172
142
  {
173
143
  "prefix_table": prefix_table,
174
- "permissions": permissions,
175
- "bulk_querystring": bulk_querystring,
176
144
  "active_tab": "prefixes",
177
145
  }
178
146
  )
179
- context.update(get_namespace_related_counts(instance, request))
180
-
181
- return context
182
-
183
-
184
- class NamespaceVRFsView(generic.ObjectView):
185
- queryset = Namespace.objects.all()
186
- template_name = "ipam/namespace_vrfs.html"
187
-
188
- def get_extra_context(self, request, instance):
189
- # Find all VRFs belonging to this Namespace
190
- vrfs = instance.vrfs.restrict(request.user, "view")
191
-
192
- vrf_table = tables.VRFTable(vrfs, exclude=["namespace"])
193
- if request.user.has_perm("ipam.change_vrf") or request.user.has_perm("ipam.delete_vrf"):
194
- vrf_table.columns.show("pk")
195
-
196
- paginate = {
197
- "paginator_class": EnhancedPaginator,
198
- "per_page": get_paginate_count(request),
199
- }
200
- RequestConfig(request, paginate).configure(vrf_table)
201
147
 
202
- # Compile permissions list for rendering the object table
203
- permissions = {
204
- "add": request.user.has_perm("ipam.add_vrf"),
205
- "change": request.user.has_perm("ipam.change_vrf"),
206
- "delete": request.user.has_perm("ipam.delete_vrf"),
207
- }
208
- bulk_querystring = f"namespace={instance.id}"
148
+ @action(detail=True, url_path="ip-addresses", url_name="ip_addresses")
149
+ def ip_addresses(self, request, *args, **kwargs):
150
+ instance = self.get_object()
151
+ ip_addresses = instance.ip_addresses.restrict(request.user, "view").select_related("role", "status", "tenant")
152
+ ip_address_table = tables.IPAddressTable(data=ip_addresses, user=request.user, exclude=["namespace"])
153
+ if request.user.has_perm("ipam.change_ipaddress") or request.user.has_perm("ipam.delete_ipaddress"):
154
+ ip_address_table.columns.show("pk")
209
155
 
210
- context = super().get_extra_context(request, instance)
211
- context.update(
156
+ RequestConfig(
157
+ request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
158
+ ).configure(ip_address_table)
159
+ return Response(
212
160
  {
213
- "vrf_table": vrf_table,
214
- "permissions": permissions,
215
- "bulk_querystring": bulk_querystring,
216
- "active_tab": "vrfs",
161
+ "ip_address_table": ip_address_table,
162
+ "active_tab": "ip_addresses",
217
163
  }
218
164
  )
219
- context.update(get_namespace_related_counts(instance, request))
220
-
221
- return context
222
165
 
223
166
 
224
167
  #
@@ -325,49 +268,32 @@ class RouteTargetUIViewSet(NautobotUIViewSet):
325
268
  #
326
269
 
327
270
 
328
- class RIRListView(generic.ObjectListView):
329
- queryset = RIR.objects.all()
330
- filterset = filters.RIRFilterSet
331
- filterset_form = forms.RIRFilterForm
332
- table = tables.RIRTable
333
-
334
-
335
- class RIRView(generic.ObjectView):
336
- queryset = RIR.objects.all()
337
-
338
- def get_extra_context(self, request, instance):
339
- # Prefixes
340
- assigned_prefixes = Prefix.objects.restrict(request.user, "view").filter(rir=instance).select_related("tenant")
341
-
342
- assigned_prefix_table = tables.PrefixTable(assigned_prefixes, hide_hierarchy_ui=True)
343
-
344
- paginate = {
345
- "paginator_class": EnhancedPaginator,
346
- "per_page": get_paginate_count(request),
347
- }
348
- RequestConfig(request, paginate).configure(assigned_prefix_table)
349
-
350
- return {"assigned_prefix_table": assigned_prefix_table, **super().get_extra_context(request, instance)}
351
-
352
-
353
- class RIREditView(generic.ObjectEditView):
271
+ class RIRUIViewSet(NautobotUIViewSet):
272
+ bulk_update_form_class = forms.RIRBulkEditForm
273
+ filterset_class = filters.RIRFilterSet
274
+ filterset_form_class = forms.RIRFilterForm
275
+ form_class = forms.RIRForm
354
276
  queryset = RIR.objects.all()
355
- model_form = forms.RIRForm
356
-
277
+ serializer_class = serializers.RIRSerializer
278
+ table_class = tables.RIRTable
357
279
 
358
- class RIRDeleteView(generic.ObjectDeleteView):
359
- queryset = RIR.objects.all()
360
-
361
-
362
- class RIRBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
363
- queryset = RIR.objects.all()
364
- table = tables.RIRTable
365
-
366
-
367
- class RIRBulkDeleteView(generic.BulkDeleteView):
368
- queryset = RIR.objects.all()
369
- filterset = filters.RIRFilterSet
370
- table = tables.RIRTable
280
+ object_detail_content = object_detail.ObjectDetailContent(
281
+ panels=(
282
+ object_detail.ObjectFieldsPanel(
283
+ section=SectionChoices.LEFT_HALF,
284
+ weight=100,
285
+ fields="__all__",
286
+ ),
287
+ object_detail.ObjectsTablePanel(
288
+ section=SectionChoices.FULL_WIDTH,
289
+ weight=100,
290
+ table_title="Assigned Prefixes",
291
+ table_class=tables.PrefixTable,
292
+ table_filter="rir",
293
+ hide_hierarchy_ui=True,
294
+ ),
295
+ ),
296
+ )
371
297
 
372
298
 
373
299
  #
@@ -1373,19 +1299,7 @@ class VLANBulkDeleteView(generic.BulkDeleteView):
1373
1299
  #
1374
1300
 
1375
1301
 
1376
- class ServiceListView(generic.ObjectListView):
1377
- queryset = Service.objects.all()
1378
- filterset = filters.ServiceFilterSet
1379
- filterset_form = forms.ServiceFilterForm
1380
- table = tables.ServiceTable
1381
- action_buttons = ("add", "import", "export")
1382
-
1383
-
1384
- class ServiceView(generic.ObjectView):
1385
- queryset = Service.objects.prefetch_related("ip_addresses")
1386
-
1387
-
1388
- class ServiceEditView(generic.ObjectEditView):
1302
+ class ServiceEditView(generic.ObjectEditView): # This view is used to assign services to devices and VMs
1389
1303
  queryset = Service.objects.prefetch_related("ip_addresses")
1390
1304
  model_form = forms.ServiceForm
1391
1305
  template_name = "ipam/service_edit.html"
@@ -1401,23 +1315,30 @@ class ServiceEditView(generic.ObjectEditView):
1401
1315
  return obj
1402
1316
 
1403
1317
 
1404
- class ServiceBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
1405
- queryset = Service.objects.all()
1406
- table = tables.ServiceTable
1318
+ class ServiceUIViewSet(NautobotUIViewSet): # 3.0 TODO: remove, unused BulkImportView
1319
+ model = Service
1320
+ bulk_update_form_class = forms.ServiceBulkEditForm
1321
+ filterset_class = filters.ServiceFilterSet
1322
+ filterset_form_class = forms.ServiceFilterForm
1323
+ form_class = forms.ServiceForm
1324
+ queryset = Service.objects.select_related("device", "virtual_machine").prefetch_related("ip_addresses")
1325
+ serializer_class = serializers.ServiceSerializer
1326
+ table_class = tables.ServiceTable
1407
1327
 
1408
-
1409
- class ServiceDeleteView(generic.ObjectDeleteView):
1410
- queryset = Service.objects.all()
1411
-
1412
-
1413
- class ServiceBulkEditView(generic.BulkEditView):
1414
- queryset = Service.objects.select_related("device", "virtual_machine")
1415
- filterset = filters.ServiceFilterSet
1416
- table = tables.ServiceTable
1417
- form = forms.ServiceBulkEditForm
1418
-
1419
-
1420
- class ServiceBulkDeleteView(generic.BulkDeleteView):
1421
- queryset = Service.objects.select_related("device", "virtual_machine")
1422
- filterset = filters.ServiceFilterSet
1423
- table = tables.ServiceTable
1328
+ object_detail_content = object_detail.ObjectDetailContent(
1329
+ panels=(
1330
+ object_detail.ObjectFieldsPanel(
1331
+ section=SectionChoices.LEFT_HALF,
1332
+ weight=100,
1333
+ fields=["name", "parent", "protocol", "port_list", "description"],
1334
+ ),
1335
+ object_detail.ObjectsTablePanel(
1336
+ weight=200,
1337
+ section=SectionChoices.RIGHT_HALF,
1338
+ table_class=tables.IPAddressTable,
1339
+ table_filter="services",
1340
+ select_related_fields=["tenant", "status", "role"],
1341
+ add_button_route=None,
1342
+ ),
1343
+ )
1344
+ )
@@ -7419,6 +7419,15 @@
7419
7419
  </span>
7420
7420
  </a>
7421
7421
 
7422
+ </li>
7423
+
7424
+ <li class="md-nav__item">
7425
+ <a href="#nautobot.apps.jobs.BaseJob.fail" class="md-nav__link">
7426
+ <span class="md-ellipsis">
7427
+ fail
7428
+ </span>
7429
+ </a>
7430
+
7422
7431
  </li>
7423
7432
 
7424
7433
  <li class="md-nav__item">
@@ -9776,6 +9785,15 @@
9776
9785
  </span>
9777
9786
  </a>
9778
9787
 
9788
+ </li>
9789
+
9790
+ <li class="md-nav__item">
9791
+ <a href="#nautobot.apps.jobs.BaseJob.fail" class="md-nav__link">
9792
+ <span class="md-ellipsis">
9793
+ fail
9794
+ </span>
9795
+ </a>
9796
+
9779
9797
  </li>
9780
9798
 
9781
9799
  <li class="md-nav__item">
@@ -10624,11 +10642,13 @@ during a approval review workflow.</p>
10624
10642
  <tbody>
10625
10643
  <tr class="doc-section-item">
10626
10644
  <td>
10627
- <code>None</code>
10645
+ <code>Any</code>
10628
10646
  </td>
10629
10647
  <td>
10630
10648
  <div class="doc-md-description">
10631
- <p>The return value of this handler is ignored.</p>
10649
+ <p>The return value of this handler is ignored normally, <strong>except</strong> if <code>self.fail()</code> is called herein,
10650
+ in which case the return value will be used as the overall JobResult return value
10651
+ since <code>self.run()</code> will <strong>not</strong> be called in such a case.</p>
10632
10652
  </div>
10633
10653
  </td>
10634
10654
  </tr>
@@ -10835,6 +10855,23 @@ path would consider this a failure of the job execution, as described in <code>n
10835
10855
  <div class="doc doc-object doc-function">
10836
10856
 
10837
10857
 
10858
+ <h3 id="nautobot.apps.jobs.BaseJob.fail" class="doc doc-heading">
10859
+ <code class="highlight language-python"><span class="n">fail</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></code>
10860
+
10861
+ <a href="#nautobot.apps.jobs.BaseJob.fail" class="headerlink" title="Permanent link">&para;</a></h3>
10862
+
10863
+
10864
+ <div class="doc doc-contents ">
10865
+
10866
+ <p>Mark this job as failed without immediately raising an exception and aborting.</p>
10867
+
10868
+ </div>
10869
+
10870
+ </div>
10871
+
10872
+ <div class="doc doc-object doc-function">
10873
+
10874
+
10838
10875
  <h3 id="nautobot.apps.jobs.BaseJob.file_path" class="doc doc-heading">
10839
10876
  <code class="highlight language-python"><span class="n">file_path</span><span class="p">()</span></code>
10840
10877
 
@@ -10914,11 +10951,12 @@ path would consider this a failure of the job execution, as described in <code>n
10914
10951
  <code>exc</code>
10915
10952
  </td>
10916
10953
  <td>
10917
- <code>Exception</code>
10954
+ <code>Any</code>
10918
10955
  </td>
10919
10956
  <td>
10920
10957
  <div class="doc-md-description">
10921
- <p>The exception raised by the task.</p>
10958
+ <p>Exception raised by the task (if any) <strong>or</strong> return value from the task, if it failed cleanly,
10959
+ such as if the Job called <code>self.fail()</code> rather than raising an exception.</p>
10922
10960
  </div>
10923
10961
  </td>
10924
10962
  <td>
@@ -10982,7 +11020,7 @@ path would consider this a failure of the job execution, as described in <code>n
10982
11020
  </td>
10983
11021
  <td>
10984
11022
  <div class="doc-md-description">
10985
- <p>Exception information.</p>
11023
+ <p>Exception information, or None.</p>
10986
11024
  </div>
10987
11025
  </td>
10988
11026
  <td>