nautobot 2.4.12__py3-none-any.whl → 2.4.13__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.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

@@ -36,16 +36,16 @@
36
36
  <a href="#theme_modal" data-toggle="modal" data-target="#theme_modal" id="btn-theme-modal"><i class="mdi mdi-theme-light-dark text-primary"></i>Theme</a> &middot;
37
37
  <i class="mdi mdi-book-open-page-variant text-primary"></i>
38
38
  {% if settings.BRANDING_URLS.docs %}
39
- <a href="{{ settings.BRANDING_URLS.docs }}">Docs</a>
39
+ <a href="{{ settings.BRANDING_URLS.docs }}" target="_blank">Docs</a>
40
40
  {% else %}
41
- <a href="{% static 'docs/index.html' %}">Docs</a>
41
+ <a href="{% static 'docs/index.html' %}" target="_blank">Docs</a>
42
42
  {% endif %}
43
43
  &middot;
44
44
  <img src="{% static 'img/jinja_logo.svg' %}" style="height:20px"> <a href="{% url 'render_jinja_template' %}">Jinja Renderer</a> &middot;
45
45
  <i class="mdi mdi-cloud-braces text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot;
46
46
  <i class="mdi mdi-graphql text-primary"></i> <a href="{% url 'graphql' %}">GraphQL</a> &middot;
47
- <i class="mdi mdi-xml text-primary"></i> <a href="{{ settings.BRANDING_URLS.code }}">Code</a> &middot;
48
- <i class="mdi mdi-lifebuoy text-primary"></i> <a href="{{ settings.BRANDING_URLS.help }}">Help</a>
47
+ <i class="mdi mdi-xml text-primary"></i> <a href="{{ settings.BRANDING_URLS.code }}" target="_blank">Code</a> &middot;
48
+ <i class="mdi mdi-lifebuoy text-primary"></i> <a href="{{ settings.BRANDING_URLS.help }}" target="_blank">Help</a>
49
49
  </p>
50
50
  {% endif %}
51
51
  </div>
@@ -0,0 +1,42 @@
1
+ # Generated by Django 4.2.20 on 2025-07-17 20:24
2
+
3
+ from django.db import migrations
4
+
5
+ import nautobot.core.models.query_functions
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ ("dcim", "0070_modulefamily_models"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterModelOptions(
15
+ name="consoleport",
16
+ options={"ordering": ("device", "module__id", "_name")},
17
+ ),
18
+ migrations.AlterModelOptions(
19
+ name="consoleserverport",
20
+ options={"ordering": ("device", "module__id", "_name")},
21
+ ),
22
+ migrations.AlterModelOptions(
23
+ name="frontport",
24
+ options={"ordering": ("device", "module__id", "_name")},
25
+ ),
26
+ migrations.AlterModelOptions(
27
+ name="interface",
28
+ options={"ordering": ("device", "module__id", nautobot.core.models.query_functions.CollateAsChar("_name"))},
29
+ ),
30
+ migrations.AlterModelOptions(
31
+ name="poweroutlet",
32
+ options={"ordering": ("device", "module__id", "_name")},
33
+ ),
34
+ migrations.AlterModelOptions(
35
+ name="powerport",
36
+ options={"ordering": ("device", "module__id", "_name")},
37
+ ),
38
+ migrations.AlterModelOptions(
39
+ name="rearport",
40
+ options={"ordering": ("device", "module__id", "_name")},
41
+ ),
42
+ ]
@@ -2,6 +2,7 @@ import re
2
2
 
3
3
  from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
4
4
  from django.contrib.contenttypes.models import ContentType
5
+ from django.core.cache import cache
5
6
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
6
7
  from django.core.validators import MaxValueValidator, MinValueValidator
7
8
  from django.db import models, transaction
@@ -123,7 +124,7 @@ class ModularComponentModel(ComponentModel):
123
124
 
124
125
  class Meta:
125
126
  abstract = True
126
- ordering = ("device", "module", "_name")
127
+ ordering = ("device", "module__id", "_name") # Module.ordering is complex/expensive so don't order by module
127
128
  constraints = [
128
129
  models.UniqueConstraint(
129
130
  fields=("device", "name"),
@@ -646,7 +647,7 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
646
647
  )
647
648
 
648
649
  class Meta(ModularComponentModel.Meta):
649
- ordering = ("device", "module", CollateAsChar("_name"))
650
+ ordering = ("device", "module__id", CollateAsChar("_name")) # Module.ordering is complex; don't order by module
650
651
 
651
652
  def clean(self):
652
653
  super().clean()
@@ -1311,3 +1312,10 @@ class ModuleBay(PrimaryModel):
1311
1312
  self.position = self.name
1312
1313
 
1313
1314
  clean.alters_data = True
1315
+
1316
+ def save(self, *args, **kwargs):
1317
+ super().save(*args, **kwargs)
1318
+
1319
+ if self.parent_device is not None:
1320
+ # Set the has_module_bays cache key on the parent device - see Device.has_module_bays()
1321
+ cache.set(f"nautobot.dcim.device.{self.parent_device.pk}.has_module_bays", True, timeout=5)
@@ -2,6 +2,7 @@ from collections import OrderedDict
2
2
 
3
3
  from django.contrib.contenttypes.fields import GenericRelation
4
4
  from django.contrib.contenttypes.models import ContentType
5
+ from django.core.cache import cache
5
6
  from django.core.exceptions import ValidationError
6
7
  from django.core.serializers.json import DjangoJSONEncoder
7
8
  from django.core.validators import MaxValueValidator, MinValueValidator
@@ -896,6 +897,8 @@ class Device(PrimaryModel, ConfigContextModel):
896
897
  instantiated_components = []
897
898
  for model, templates in component_models:
898
899
  model.objects.bulk_create([x.instantiate(device=self) for x in templates])
900
+ cache_key = f"nautobot.dcim.device.{self.pk}.has_module_bays"
901
+ cache.delete(cache_key)
899
902
  return instantiated_components
900
903
 
901
904
  create_components.alters_data = True
@@ -988,6 +991,18 @@ class Device(PrimaryModel, ConfigContextModel):
988
991
  """
989
992
  return Device.objects.filter(parent_bay__device=self.pk)
990
993
 
994
+ @property
995
+ def has_module_bays(self) -> bool:
996
+ """
997
+ Cacheable property for determining whether this Device has any ModuleBays, and therefore may contain Modules.
998
+ """
999
+ cache_key = f"nautobot.dcim.device.{self.pk}.has_module_bays"
1000
+ module_bays_exists = cache.get(cache_key)
1001
+ if module_bays_exists is None:
1002
+ module_bays_exists = self.module_bays.exists()
1003
+ cache.set(cache_key, module_bays_exists, timeout=5)
1004
+ return module_bays_exists
1005
+
991
1006
  @property
992
1007
  def all_modules(self):
993
1008
  """
@@ -998,6 +1013,9 @@ class Device(PrimaryModel, ConfigContextModel):
998
1013
  # We artificially limit the recursion to 4 levels or we would be stuck in an infinite loop.
999
1014
  recursion_depth = MODULE_RECURSION_DEPTH_LIMIT
1000
1015
  qs = Module.objects.all()
1016
+ if not self.has_module_bays:
1017
+ # Short-circuit to avoid an expensive nested query
1018
+ return qs.none()
1001
1019
  query = Q()
1002
1020
  for level in range(recursion_depth):
1003
1021
  recursive_query = "parent_module_bay__parent_module__" * level
@@ -398,40 +398,7 @@
398
398
  </div>
399
399
  </div>
400
400
  {% endif %}
401
- {% if object.is_dynamic_group_associable_model and perms.extras.view_dynamicgroup %}
402
- <div id="dynamic_groups" role="tabpanel" class="tab-pane {% if request.GET.tab == 'dynamic_groups' %}active{% else %}fade{% endif %}">
403
- <div class="row">
404
- <div class="col-md-12">
405
- <div class="alert alert-warning">
406
- Dynamic group membership is cached for performance reasons,
407
- therefore this table may not always be up-to-date.
408
- <br>You can refresh the membership of any specific group by navigating to it from the list below
409
- or from the <a href="{% url 'extras:dynamicgroup_list' %}">Dynamic Groups list view</a>.
410
- <br>You can also refresh the membership of all groups by running the
411
- <a href="{% url 'extras:job_run_by_class_path' class_path='nautobot.core.jobs.groups.RefreshDynamicGroupCaches' %}">Refresh Dynamic Group Caches job</a>.
412
- </div>
413
- </div>
414
- </div>
415
- <div class="row">
416
- <div class="col-md-12">
417
- <form method="post">
418
- {% csrf_token %}
419
- <div class="panel panel-default">
420
- <div class="panel-heading">
421
- <strong>Dynamic Groups</strong>
422
- <div class="pull-right noprint">
423
- <!-- Insert table config button here -->
424
- </div>
425
- </div>
426
- <div class="table-responsive">
427
- {% render_table associated_dynamic_groups_table 'inc/table.html' %}
428
- </div>
429
- </div>
430
- </form>
431
- </div>
432
- </div>
433
- </div>
434
- {% endif %}
401
+ {% comment %}The dynamic_groups tab is intentionally omitted here for performance reasons.{% endcomment %}
435
402
  {% if object.is_metadata_associable_model and perms.extras.view_objectmetadata %}
436
403
  <div id="object_metadata" role="tabpanel" class="tab-pane {% if request.GET.tab == 'object_metadata' %}active{% else %}fade{% endif %}">
437
404
  <div class="row">
@@ -3,8 +3,11 @@
3
3
  {% load static %}
4
4
 
5
5
  {% block buttons %}
6
+ <button class="btn btn-default toggle-fullname" selected="selected">
7
+ <span class="mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Device Full Name
8
+ </button>
6
9
  <button class="btn btn-default toggle-images" selected="selected">
7
- <span class="mdi mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
10
+ <span class="mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
8
11
  </button>
9
12
  <div class="btn-group" role="group">
10
13
  <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
@@ -1778,6 +1778,7 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1778
1778
  self.device.validated_save()
1779
1779
 
1780
1780
  def test_all_x_properties(self):
1781
+ self.assertTrue(self.device.has_module_bays)
1781
1782
  self.assertEqual(self.device.all_modules.count(), 0)
1782
1783
  self.assertEqual(self.device.all_module_bays.count(), 1)
1783
1784
  self.assertEqual(self.device.all_console_server_ports.count(), 1)
nautobot/dcim/urls.py CHANGED
@@ -493,7 +493,7 @@ urlpatterns = [
493
493
  name="device_notes",
494
494
  kwargs={"model": Device},
495
495
  ),
496
- path( # 3.0 TODO: remove, no longer needed/used since 2.3
496
+ path(
497
497
  "devices/<uuid:pk>/dynamic-groups/",
498
498
  views.DeviceDynamicGroupsView.as_view(),
499
499
  name="device_dynamicgroups",
nautobot/dcim/views.py CHANGED
@@ -59,6 +59,7 @@ from nautobot.core.views.viewsets import NautobotUIViewSet
59
59
  from nautobot.dcim.choices import LocationDataToContactActionChoices
60
60
  from nautobot.dcim.forms import LocationMigrateDataToContactForm
61
61
  from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
62
+ from nautobot.extras.tables import DynamicGroupTable
62
63
  from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
63
64
  from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
64
65
  from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable, VRFTable
@@ -1701,7 +1702,39 @@ class DeviceView(generic.ObjectView):
1701
1702
  "tenant__tenant_group",
1702
1703
  ).prefetch_related("images", "software_image_files")
1703
1704
 
1704
- object_detail_content = object_detail.ObjectDetailContent(
1705
+ class DeviceDetailContent(object_detail.ObjectDetailContent):
1706
+ """
1707
+ Override base ObjectDetailContent to render dynamic-groups table as a separate view/tab instead of inline.
1708
+ """
1709
+
1710
+ def __init__(self, **kwargs):
1711
+ super().__init__(**kwargs)
1712
+ # Remove inline tab definition
1713
+ for tab in list(self._tabs):
1714
+ if isinstance(tab, object_detail._ObjectDetailGroupsTab):
1715
+ self._tabs.remove(tab)
1716
+ # Add distinct-view tab definition
1717
+ self._tabs.append(
1718
+ object_detail.DistinctViewTab(
1719
+ weight=object_detail.Tab.WEIGHT_GROUPS_TAB,
1720
+ tab_id="dynamic_groups",
1721
+ label="Dynamic Groups",
1722
+ url_name="dcim:device_dynamicgroups",
1723
+ related_object_attribute="dynamic_groups",
1724
+ panels=(
1725
+ object_detail.ObjectsTablePanel(
1726
+ weight=100,
1727
+ table_class=DynamicGroupTable,
1728
+ table_attribute="dynamic_groups",
1729
+ exclude_columns=["content_type"],
1730
+ add_button_route=None,
1731
+ related_field_name="member_id",
1732
+ ),
1733
+ ),
1734
+ )
1735
+ )
1736
+
1737
+ object_detail_content = DeviceDetailContent(
1705
1738
  extra_buttons=(
1706
1739
  object_detail.DropdownButton(
1707
1740
  weight=100,
@@ -2188,7 +2221,7 @@ class DeviceChangeLogView(ObjectChangeLogView):
2188
2221
  base_template = "dcim/device/base.html"
2189
2222
 
2190
2223
 
2191
- class DeviceDynamicGroupsView(ObjectDynamicGroupsView): # 3.0 TODO: remove, deprecated in 2.3
2224
+ class DeviceDynamicGroupsView(ObjectDynamicGroupsView):
2192
2225
  base_template = "dcim/device/base.html"
2193
2226
 
2194
2227
 
nautobot/extras/views.py CHANGED
@@ -945,11 +945,13 @@ class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
945
945
  filterset = filters.DynamicGroupFilterSet
946
946
 
947
947
 
948
- # 3.0 TODO: remove, deprecated since 2.3 (#5845)
949
948
  class ObjectDynamicGroupsView(generic.GenericView):
950
949
  """
951
950
  Present a list of dynamic groups associated to a particular object.
952
951
 
952
+ Note that this isn't currently widely used, as most object detail views currently render the table inline
953
+ rather than using this separate view. This may change in the future.
954
+
953
955
  base_template: Specify to explicitly identify the base object detail template to render.
954
956
  If not provided, "<app>/<model>.html", "<app>/<model>_retrieve.html", or "generic/object_retrieve.html"
955
957
  will be used, as per `get_base_template()`.
@@ -969,7 +971,6 @@ class ObjectDynamicGroupsView(generic.GenericView):
969
971
  data=obj.dynamic_groups.restrict(request.user, "view"), orderable=False
970
972
  )
971
973
  dynamicgroups_table.columns.hide("content_type")
972
- dynamicgroups_table.columns.hide("members")
973
974
 
974
975
  # Apply the request context
975
976
  paginate = {
@@ -0,0 +1,28 @@
1
+ # Generated by Django 4.2.20 on 2025-07-17 16:10
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("ipam", "0051_added_optional_vrf_relationship_to_vdc"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterIndexTogether(
13
+ name="ipaddress",
14
+ index_together={("ip_version", "host", "mask_length")},
15
+ ),
16
+ migrations.AlterIndexTogether(
17
+ name="prefix",
18
+ index_together={
19
+ ("namespace", "ip_version", "network", "prefix_length"),
20
+ ("namespace", "network", "broadcast", "prefix_length"),
21
+ ("network", "broadcast", "prefix_length"),
22
+ },
23
+ ),
24
+ migrations.AlterIndexTogether(
25
+ name="vrf",
26
+ index_together={("namespace", "name", "rd")},
27
+ ),
28
+ ]
nautobot/ipam/models.py CHANGED
@@ -170,6 +170,9 @@ class VRF(PrimaryModel):
170
170
  # where multiple different-RD VRFs with the same name may already exist.
171
171
  # ["namespace", "name"],
172
172
  ]
173
+ index_together = [
174
+ ["namespace", "name", "rd"],
175
+ ]
173
176
  verbose_name = "VRF"
174
177
  verbose_name_plural = "VRFs"
175
178
 
@@ -409,7 +412,12 @@ class VRFPrefixAssignment(BaseModel):
409
412
  super().clean()
410
413
 
411
414
  if self.prefix.namespace != self.vrf.namespace:
412
- raise ValidationError({"prefix": "Prefix must be in same namespace as VRF"})
415
+ raise ValidationError(
416
+ {
417
+ "prefix": f"Prefix (namespace {self.prefix.namespace}) must be in same namespace as "
418
+ "VRF (namespace {self.vrf.namespace})"
419
+ }
420
+ )
413
421
 
414
422
 
415
423
  @extras_features(
@@ -589,6 +597,7 @@ class Prefix(PrimaryModel):
589
597
  index_together = [
590
598
  ["network", "broadcast", "prefix_length"],
591
599
  ["namespace", "network", "broadcast", "prefix_length"],
600
+ ["namespace", "ip_version", "network", "prefix_length"],
592
601
  ]
593
602
  unique_together = ["namespace", "network", "prefix_length"]
594
603
  verbose_name_plural = "prefixes"
@@ -1250,6 +1259,9 @@ class IPAddress(PrimaryModel):
1250
1259
  verbose_name = "IP address"
1251
1260
  verbose_name_plural = "IP addresses"
1252
1261
  unique_together = ["parent", "host"]
1262
+ index_together = [
1263
+ ["ip_version", "host", "mask_length"],
1264
+ ]
1253
1265
 
1254
1266
  def __init__(self, *args, address=None, namespace=None, **kwargs):
1255
1267
  super().__init__(*args, **kwargs)
@@ -316,9 +316,7 @@ class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
316
316
  response, "The fields vrf, prefix must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
317
317
  )
318
318
  response = self.client.post(self._get_list_url(), wrong_namespace_create_data, format="json", **self.header)
319
- self.assertContains(
320
- response, "Prefix must be in same namespace as VRF", status_code=status.HTTP_400_BAD_REQUEST
321
- )
319
+ self.assertContains(response, "must be in same namespace as", status_code=status.HTTP_400_BAD_REQUEST)
322
320
  response = self.client.post(self._get_list_url(), missing_field_create_data, format="json", **self.header)
323
321
  self.assertContains(response, "This field may not be null.", status_code=status.HTTP_400_BAD_REQUEST)
324
322
 
@@ -5,6 +5,8 @@ import random
5
5
  from django.apps import apps
6
6
  from netaddr import IPNetwork
7
7
 
8
+ from nautobot.ipam.models import get_default_namespace
9
+
8
10
  # Calculate the probabilities to use for the maybe_subdivide() function defined below.
9
11
 
10
12
  # Frequency of IPv4 (leaf, network) Prefixes by each given mask length in a "realistic" data set.
@@ -113,40 +115,73 @@ def create_prefixes_and_ips(initial_subnet: str, apps=apps, seed="Nautobot"): #
113
115
  status_active, _ = Status.objects.get_or_create(name="Active", defaults={"slug": "active"})
114
116
 
115
117
  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
+ Tenant.objects.get_or_create(name=f"{initial_subnet} Tenant {i}")
119
+ if hasattr(VRF, "enforce_unique"):
120
+ VRF.objects.get_or_create(
121
+ name=f"{initial_subnet} VRF {i}",
122
+ enforce_unique=False, # TODO should some enforce_unique?
123
+ )
124
+ else:
125
+ VRF.objects.get_or_create(name=f"{initial_subnet} VRF {i}")
118
126
 
119
127
  all_tenants = list(Tenant.objects.all())
120
- all_vrfs = list(VRF.objects.all())
128
+ if hasattr(VRF, "namespace"):
129
+ all_vrfs = list(VRF.objects.filter(namespace=get_default_namespace()))
130
+ else:
131
+ all_vrfs = list(VRF.objects.all())
132
+
133
+ create_prefixes(initial_subnet, all_tenants, all_vrfs, status_active, Prefix)
134
+ create_ips(initial_subnet, all_tenants, all_vrfs, status_active, IPAddress)
121
135
 
136
+
137
+ def create_prefixes(initial_subnet, all_tenants, all_vrfs, status_active, Prefix):
122
138
  print(f"Creating Prefixes to subdivide {initial_subnet}")
123
139
  unique_prefix_count = 0
124
140
  duplicate_prefix_count = 0
125
141
  for subnet in maybe_subdivide(IPNetwork(initial_subnet)):
126
142
  if random.random() < 0.95: # noqa: S311 # suspicious-non-cryptographic-random-usage
127
143
  # 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(
144
+ vrf = maybe_random_instance(all_vrfs)
145
+ if hasattr(Prefix, "vrf"):
146
+ Prefix.objects.get_or_create(
140
147
  network=str(subnet.network),
141
148
  broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
142
149
  prefix_length=subnet.prefixlen,
143
150
  status=status_active,
144
151
  tenant=maybe_random_instance(all_tenants),
145
- vrf=maybe_random_instance(all_vrfs),
152
+ vrf=vrf,
146
153
  )
147
- duplicate_prefix_count += 1
154
+ else:
155
+ prefix, _ = Prefix.objects.get_or_create(
156
+ network=str(subnet.network),
157
+ broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
158
+ prefix_length=subnet.prefixlen,
159
+ status=status_active,
160
+ tenant=maybe_random_instance(all_tenants),
161
+ )
162
+ if vrf is not None:
163
+ vrf.add_prefix(prefix)
164
+
165
+ unique_prefix_count += 1
166
+ if hasattr(Prefix, "vrf"):
167
+ while random.random() < 0.1: # noqa: S311 # suspicious-non-cryptographic-random-usage
168
+ # 10% repeating chance to create a duplicate(s) of this Prefix
169
+ Prefix.objects.create(
170
+ network=str(subnet.network),
171
+ broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
172
+ prefix_length=subnet.prefixlen,
173
+ status=status_active,
174
+ tenant=maybe_random_instance(all_tenants),
175
+ vrf=maybe_random_instance(all_vrfs),
176
+ )
177
+ duplicate_prefix_count += 1
178
+ else:
179
+ # TODO: create prefixes in different namespaces?
180
+ pass
148
181
  print(f"Created {unique_prefix_count} unique Prefixes and {duplicate_prefix_count} duplicates")
149
182
 
183
+
184
+ def create_ips(initial_subnet, all_tenants, all_vrfs, status_active, IPAddress):
150
185
  print(f"Creating IPAddresses within {initial_subnet}")
151
186
  unique_ip_count = 0
152
187
  duplicate_ip_count = 0
@@ -154,17 +189,7 @@ def create_prefixes_and_ips(initial_subnet: str, apps=apps, seed="Nautobot"): #
154
189
  if random.random() < 0.05: # noqa: S311 # suspicious-non-cryptographic-random-usage
155
190
  # 5% chance to create any given IP address
156
191
  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
192
+ if hasattr(IPAddress, "prefix_length"):
168
193
  IPAddress.objects.create(
169
194
  host=str(network.ip),
170
195
  broadcast=str(network.broadcast if network.broadcast else network[-1]),
@@ -173,5 +198,27 @@ def create_prefixes_and_ips(initial_subnet: str, apps=apps, seed="Nautobot"): #
173
198
  tenant=maybe_random_instance(all_tenants),
174
199
  vrf=maybe_random_instance(all_vrfs),
175
200
  )
176
- duplicate_ip_count += 1
201
+ else:
202
+ IPAddress.objects.create(
203
+ host=str(network.ip),
204
+ mask_length=network.prefixlen,
205
+ status=status_active,
206
+ tenant=maybe_random_instance(all_tenants),
207
+ )
208
+ unique_ip_count += 1
209
+ if hasattr(IPAddress, "prefix_length"):
210
+ while random.random() < 0.1: # noqa: S311 # suspicious-non-cryptographic-random-usage
211
+ # 10% repeating chance to create a duplicate(s) of this IP
212
+ IPAddress.objects.create(
213
+ host=str(network.ip),
214
+ broadcast=str(network.broadcast if network.broadcast else network[-1]),
215
+ prefix_length=network.prefixlen,
216
+ status=status_active,
217
+ tenant=maybe_random_instance(all_tenants),
218
+ vrf=maybe_random_instance(all_vrfs),
219
+ )
220
+ duplicate_ip_count += 1
221
+ else:
222
+ # TODO: create duplicate IPs in other namespaces?
223
+ pass
177
224
  print(f"Created {unique_ip_count} unique IPAddresses and {duplicate_ip_count} duplicates")
@@ -13617,6 +13617,8 @@ Override to add more variables to Response</p>
13617
13617
 
13618
13618
 
13619
13619
  <p>Present a list of dynamic groups associated to a particular object.</p>
13620
+ <p>Note that this isn't currently widely used, as most object detail views currently render the table inline
13621
+ rather than using this separate view. This may change in the future.</p>
13620
13622
 
13621
13623
 
13622
13624
  <details class="base_template" open>