nautobot 2.4.5__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 (115) hide show
  1. nautobot/core/api/mixins.py +10 -0
  2. nautobot/core/celery/encoders.py +2 -2
  3. nautobot/core/forms/fields.py +21 -5
  4. nautobot/core/forms/utils.py +1 -0
  5. nautobot/core/jobs/bulk_actions.py +1 -1
  6. nautobot/core/management/commands/generate_test_data.py +1 -1
  7. nautobot/core/models/name_color_content_types.py +9 -0
  8. nautobot/core/models/validators.py +7 -0
  9. nautobot/core/settings.py +0 -14
  10. nautobot/core/settings.yaml +0 -28
  11. nautobot/core/tables.py +6 -1
  12. nautobot/core/templates/generic/object_retrieve.html +1 -1
  13. nautobot/core/testing/api.py +18 -0
  14. nautobot/core/tests/nautobot_config.py +0 -2
  15. nautobot/core/tests/runner.py +17 -140
  16. nautobot/core/tests/test_api.py +4 -4
  17. nautobot/core/tests/test_authentication.py +83 -4
  18. nautobot/core/tests/test_forms.py +11 -8
  19. nautobot/core/tests/test_graphql.py +9 -0
  20. nautobot/core/tests/test_jobs.py +7 -0
  21. nautobot/core/ui/object_detail.py +31 -0
  22. nautobot/dcim/factory.py +2 -0
  23. nautobot/dcim/filters/__init__.py +5 -0
  24. nautobot/dcim/forms.py +17 -1
  25. nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
  26. nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
  27. nautobot/dcim/models/devices.py +9 -2
  28. nautobot/dcim/tables/devices.py +1 -0
  29. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
  30. nautobot/dcim/tests/test_api.py +74 -31
  31. nautobot/dcim/tests/test_filters.py +2 -0
  32. nautobot/dcim/tests/test_models.py +65 -0
  33. nautobot/dcim/tests/test_views.py +3 -0
  34. nautobot/extras/forms/forms.py +7 -3
  35. nautobot/extras/plugins/marketplace_manifest.yml +18 -0
  36. nautobot/extras/tables.py +4 -5
  37. nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
  38. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  39. nautobot/extras/templates/extras/status.html +1 -37
  40. nautobot/extras/tests/integration/test_notes.py +1 -1
  41. nautobot/extras/tests/test_api.py +22 -7
  42. nautobot/extras/tests/test_changelog.py +4 -4
  43. nautobot/extras/tests/test_customfields.py +3 -0
  44. nautobot/extras/tests/test_plugins.py +19 -13
  45. nautobot/extras/tests/test_relationships.py +9 -0
  46. nautobot/extras/tests/test_tags.py +2 -2
  47. nautobot/extras/tests/test_views.py +15 -6
  48. nautobot/extras/urls.py +1 -30
  49. nautobot/extras/views.py +10 -54
  50. nautobot/ipam/tables.py +6 -2
  51. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
  52. nautobot/ipam/templates/ipam/service.html +2 -46
  53. nautobot/ipam/templates/ipam/service_edit.html +1 -17
  54. nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
  55. nautobot/ipam/tests/migration/__init__.py +0 -0
  56. nautobot/ipam/tests/migration/test_migrations.py +510 -0
  57. nautobot/ipam/tests/test_api.py +66 -36
  58. nautobot/ipam/tests/test_filters.py +0 -10
  59. nautobot/ipam/tests/test_views.py +44 -2
  60. nautobot/ipam/urls.py +2 -47
  61. nautobot/ipam/utils/migrations.py +185 -152
  62. nautobot/ipam/utils/testing.py +177 -0
  63. nautobot/ipam/views.py +95 -157
  64. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
  65. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
  66. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
  67. nautobot/project-static/docs/development/apps/api/testing.html +0 -87
  68. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  69. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  70. nautobot/project-static/docs/development/core/getting-started.html +78 -107
  71. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  72. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  73. nautobot/project-static/docs/development/core/testing.html +24 -198
  74. nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
  75. nautobot/project-static/docs/objects.inv +0 -0
  76. nautobot/project-static/docs/overview/application_stack.html +1 -1
  77. nautobot/project-static/docs/release-notes/version-2.4.html +226 -1
  78. nautobot/project-static/docs/search/search_index.json +1 -1
  79. nautobot/project-static/docs/sitemap.xml +290 -290
  80. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  81. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
  82. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
  83. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
  84. nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
  85. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  86. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
  87. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
  88. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
  89. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
  90. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
  91. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
  92. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  93. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  94. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
  95. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  96. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
  97. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  98. nautobot/project-static/docs/user-guide/index.html +89 -2
  99. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
  100. nautobot/virtualization/forms.py +20 -0
  101. nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
  102. nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
  103. nautobot/virtualization/tests/test_api.py +14 -3
  104. nautobot/virtualization/tests/test_views.py +10 -2
  105. nautobot/virtualization/urls.py +10 -93
  106. nautobot/virtualization/views.py +33 -72
  107. {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/METADATA +6 -5
  108. {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/RECORD +113 -108
  109. {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
  110. nautobot/core/tests/performance_baselines.yml +0 -8900
  111. nautobot/ipam/tests/test_migrations.py +0 -462
  112. /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
  113. {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
  114. {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
  115. {nautobot-2.4.5.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
-
141
- return context
142
-
143
-
144
- class NamespacePrefixesView(generic.ObjectView):
145
- queryset = Namespace.objects.all()
146
- template_name = "ipam/namespace_prefixes.html"
147
129
 
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
147
 
196
- paginate = {
197
- "paginator_class": EnhancedPaginator,
198
- "per_page": get_paginate_count(request),
199
- }
200
- RequestConfig(request, paginate).configure(vrf_table)
201
-
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
  #
@@ -1356,19 +1299,7 @@ class VLANBulkDeleteView(generic.BulkDeleteView):
1356
1299
  #
1357
1300
 
1358
1301
 
1359
- class ServiceListView(generic.ObjectListView):
1360
- queryset = Service.objects.all()
1361
- filterset = filters.ServiceFilterSet
1362
- filterset_form = forms.ServiceFilterForm
1363
- table = tables.ServiceTable
1364
- action_buttons = ("add", "import", "export")
1365
-
1366
-
1367
- class ServiceView(generic.ObjectView):
1368
- queryset = Service.objects.prefetch_related("ip_addresses")
1369
-
1370
-
1371
- class ServiceEditView(generic.ObjectEditView):
1302
+ class ServiceEditView(generic.ObjectEditView): # This view is used to assign services to devices and VMs
1372
1303
  queryset = Service.objects.prefetch_related("ip_addresses")
1373
1304
  model_form = forms.ServiceForm
1374
1305
  template_name = "ipam/service_edit.html"
@@ -1384,23 +1315,30 @@ class ServiceEditView(generic.ObjectEditView):
1384
1315
  return obj
1385
1316
 
1386
1317
 
1387
- class ServiceBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
1388
- queryset = Service.objects.all()
1389
- table = tables.ServiceTable
1390
-
1391
-
1392
- class ServiceDeleteView(generic.ObjectDeleteView):
1393
- queryset = Service.objects.all()
1394
-
1395
-
1396
- class ServiceBulkEditView(generic.BulkEditView):
1397
- queryset = Service.objects.select_related("device", "virtual_machine")
1398
- filterset = filters.ServiceFilterSet
1399
- table = tables.ServiceTable
1400
- form = forms.ServiceBulkEditForm
1401
-
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
1402
1327
 
1403
- class ServiceBulkDeleteView(generic.BulkDeleteView):
1404
- queryset = Service.objects.select_related("device", "virtual_machine")
1405
- filterset = filters.ServiceFilterSet
1406
- 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
+ )
@@ -7904,6 +7904,21 @@
7904
7904
  </span>
7905
7905
  </a>
7906
7906
 
7907
+ <nav class="md-nav" aria-label="EnhancedURLValidator">
7908
+ <ul class="md-nav__list">
7909
+
7910
+ <li class="md-nav__item">
7911
+ <a href="#nautobot.apps.models.EnhancedURLValidator.__getattribute__" class="md-nav__link">
7912
+ <span class="md-ellipsis">
7913
+ __getattribute__
7914
+ </span>
7915
+ </a>
7916
+
7917
+ </li>
7918
+
7919
+ </ul>
7920
+ </nav>
7921
+
7907
7922
  </li>
7908
7923
 
7909
7924
  <li class="md-nav__item">
@@ -10981,6 +10996,21 @@
10981
10996
  </span>
10982
10997
  </a>
10983
10998
 
10999
+ <nav class="md-nav" aria-label="EnhancedURLValidator">
11000
+ <ul class="md-nav__list">
11001
+
11002
+ <li class="md-nav__item">
11003
+ <a href="#nautobot.apps.models.EnhancedURLValidator.__getattribute__" class="md-nav__link">
11004
+ <span class="md-ellipsis">
11005
+ __getattribute__
11006
+ </span>
11007
+ </a>
11008
+
11009
+ </li>
11010
+
11011
+ </ul>
11012
+ </nav>
11013
+
10984
11014
  </li>
10985
11015
 
10986
11016
  <li class="md-nav__item">
@@ -13342,6 +13372,23 @@ schemes specified in the configuration.</p>
13342
13372
 
13343
13373
 
13344
13374
 
13375
+ <div class="doc doc-object doc-function">
13376
+
13377
+
13378
+ <h3 id="nautobot.apps.models.EnhancedURLValidator.__getattribute__" class="doc doc-heading">
13379
+ <code class="highlight language-python"><span class="fm">__getattribute__</span><span class="p">(</span><span class="n">name</span><span class="p">)</span></code>
13380
+
13381
+ <a href="#nautobot.apps.models.EnhancedURLValidator.__getattribute__" class="headerlink" title="Permanent link">&para;</a></h3>
13382
+
13383
+
13384
+ <div class="doc doc-contents ">
13385
+
13386
+ <p>Dynamically fetch schemes each time it's accessed.</p>
13387
+
13388
+ </div>
13389
+
13390
+ </div>
13391
+
13345
13392
 
13346
13393
 
13347
13394
  </div>
@@ -10323,6 +10323,24 @@ If not specified, the first key in <code>url_params</code> will be implicitly us
10323
10323
  <code>False</code>
10324
10324
  </td>
10325
10325
  </tr>
10326
+ <tr class="doc-section-item">
10327
+ <td>
10328
+ <code>display_field</code>
10329
+ </td>
10330
+ <td>
10331
+ <code>str</code>
10332
+ </td>
10333
+ <td>
10334
+ <div class="doc-md-description">
10335
+ <p>Name of the field to use when displaying an object rather than just a count.
10336
+ This will be passed to hyperlinked_object() as the <code>field</code> parameter
10337
+ If not specified, it will use the "display" field.</p>
10338
+ </div>
10339
+ </td>
10340
+ <td>
10341
+ <code>&#39;display&#39;</code>
10342
+ </td>
10343
+ </tr>
10326
10344
  <tr class="doc-section-item">
10327
10345
  <td>
10328
10346
  <code>**kwargs</code>
@@ -12583,6 +12583,69 @@ Mutually exclusive with <code>column_headers</code>.</p>
12583
12583
  <p>A Tab that doesn't render inline on the same page, but instead links to a distinct view of its own when clicked.</p>
12584
12584
 
12585
12585
 
12586
+ <p><span class="doc-section-title">Parameters:</span></p>
12587
+ <table>
12588
+ <thead>
12589
+ <tr>
12590
+ <th>Name</th>
12591
+ <th>Type</th>
12592
+ <th>Description</th>
12593
+ <th>Default</th>
12594
+ </tr>
12595
+ </thead>
12596
+ <tbody>
12597
+ <tr class="doc-section-item">
12598
+ <td>
12599
+ <code>url_name</code>
12600
+ </td>
12601
+ <td>
12602
+ <code>str</code>
12603
+ </td>
12604
+ <td>
12605
+ <div class="doc-md-description">
12606
+ <p>The name of the URL pattern to link to, which will be reversed to generate the URL.</p>
12607
+ </div>
12608
+ </td>
12609
+ <td>
12610
+ <em>required</em>
12611
+ </td>
12612
+ </tr>
12613
+ <tr class="doc-section-item">
12614
+ <td>
12615
+ <code>label_wrapper_template_path</code>
12616
+ </td>
12617
+ <td>
12618
+ <code>str</code>
12619
+ </td>
12620
+ <td>
12621
+ <div class="doc-md-description">
12622
+ <p>Template path to render the tab label to HTML.</p>
12623
+ </div>
12624
+ </td>
12625
+ <td>
12626
+ <code>&#39;components/tab/label_wrapper_distinct_view.html&#39;</code>
12627
+ </td>
12628
+ </tr>
12629
+ <tr class="doc-section-item">
12630
+ <td>
12631
+ <code>related_object_attribute</code>
12632
+ </td>
12633
+ <td>
12634
+ <code>str</code>
12635
+ </td>
12636
+ <td>
12637
+ <div class="doc-md-description">
12638
+ <p>The name of the related object attribute to count for the tab label.</p>
12639
+ </div>
12640
+ </td>
12641
+ <td>
12642
+ <code>&#39;&#39;</code>
12643
+ </td>
12644
+ </tr>
12645
+ </tbody>
12646
+ </table>
12647
+
12648
+
12586
12649
 
12587
12650
 
12588
12651