nautobot 1.6.25__py3-none-any.whl → 1.6.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/core/management/commands/pre_migrate.py +44 -24
- nautobot/core/tests/test_commands.py +37 -0
- nautobot/dcim/models/devices.py +10 -3
- nautobot/dcim/tests/test_models.py +74 -0
- nautobot/project-static/docs/release-notes/version-1.6.html +216 -108
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +187 -187
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- {nautobot-1.6.25.dist-info → nautobot-1.6.27.dist-info}/METADATA +1 -1
- {nautobot-1.6.25.dist-info → nautobot-1.6.27.dist-info}/RECORD +14 -14
- {nautobot-1.6.25.dist-info → nautobot-1.6.27.dist-info}/LICENSE.txt +0 -0
- {nautobot-1.6.25.dist-info → nautobot-1.6.27.dist-info}/NOTICE +0 -0
- {nautobot-1.6.25.dist-info → nautobot-1.6.27.dist-info}/WHEEL +0 -0
- {nautobot-1.6.25.dist-info → nautobot-1.6.27.dist-info}/entry_points.txt +0 -0
|
@@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
7
7
|
from django.core.exceptions import ValidationError
|
|
8
8
|
from django.core.management.base import BaseCommand, CommandError
|
|
9
9
|
from django.db import models
|
|
10
|
+
from django.db.models.fields.json import KeyTransform
|
|
10
11
|
|
|
11
12
|
from nautobot.circuits.models import Circuit
|
|
12
13
|
from nautobot.dcim.models import (
|
|
@@ -376,42 +377,61 @@ def check_permissions_constraints(command):
|
|
|
376
377
|
continue
|
|
377
378
|
|
|
378
379
|
for lookup in qs._query.where.children:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
380
|
+
lhs = lookup.lhs
|
|
381
|
+
|
|
382
|
+
# Check if the left-hand side (lhs) of the lookup is a KeyTransform.
|
|
383
|
+
# KeyTransform is used for accessing nested keys in JSON fields.
|
|
384
|
+
if isinstance(lhs, KeyTransform):
|
|
385
|
+
# If lhs is a KeyTransform, it means we're dealing with a JSON field lookup usually a CustomField.
|
|
386
|
+
# `lhs.source_expressions` returns a list of `Col` objects representing the columns involved in the lookup.
|
|
387
|
+
cols = lhs.source_expressions
|
|
388
|
+
# - `col.field.name` gives the base field name (_custom_field_data).
|
|
389
|
+
# - `lhs.key_name` provides the specific custom field name (test_custom_field).
|
|
390
|
+
# Construct the field name by combining these with a double underscore.
|
|
391
|
+
suffix = f"__{lhs.key_name}"
|
|
392
|
+
else:
|
|
393
|
+
# If lhs is not a KeyTransform, it is a direct column reference.
|
|
394
|
+
# Wrap lhs in a list to handle it uniformly in the next step.
|
|
395
|
+
cols = [lhs]
|
|
396
|
+
suffix = ""
|
|
397
|
+
|
|
398
|
+
for col in cols:
|
|
399
|
+
field_name = f"{col.field.name}{suffix}"
|
|
400
|
+
related_model = col.target.related_model or col.target.model
|
|
401
|
+
|
|
402
|
+
if related_model in replaced_models:
|
|
392
403
|
cls = f"{related_model.__module__}.{related_model.__name__}"
|
|
393
404
|
warnings.append(
|
|
394
405
|
f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
|
|
395
|
-
f"a model
|
|
406
|
+
f"a model ({cls}) that will be migrated to a new model by the Nautobot 2.0 migration.\n"
|
|
396
407
|
+ json.dumps(perm.constraints, indent=4)
|
|
397
408
|
)
|
|
398
409
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
410
|
+
if related_model in field_change_models:
|
|
411
|
+
if col.field.name in field_change_models[related_model]:
|
|
412
|
+
cls = f"{related_model.__module__}.{related_model.__name__}"
|
|
413
|
+
warnings.append(
|
|
414
|
+
f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
|
|
415
|
+
f"a model field ({cls}.{field_name}) that may be changed by the Nautobot 2.0 migration.\n"
|
|
416
|
+
+ json.dumps(perm.constraints, indent=4)
|
|
417
|
+
)
|
|
405
418
|
|
|
406
|
-
|
|
407
|
-
if isinstance(lookup.lhs.target, (models.fields.related.RelatedField, models.fields.UUIDField)):
|
|
408
|
-
cls = f"{related_model.__module__}.{related_model.__name__}"
|
|
419
|
+
if field_name == "slug" and related_model is not GitRepository:
|
|
409
420
|
warnings.append(
|
|
410
421
|
f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
|
|
411
|
-
f"
|
|
422
|
+
f"a 'slug' field that will be deleted by the Nautobot 2.0 migration.\n"
|
|
412
423
|
+ json.dumps(perm.constraints, indent=4)
|
|
413
424
|
)
|
|
414
425
|
|
|
426
|
+
if related_model in pk_change_models:
|
|
427
|
+
if isinstance(col.target, (models.fields.related.RelatedField, models.fields.UUIDField)):
|
|
428
|
+
cls = f"{related_model.__module__}.{related_model.__name__}"
|
|
429
|
+
warnings.append(
|
|
430
|
+
f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
|
|
431
|
+
f"an object ({cls}) that may have its primary key changed by the Nautobot 2.0 migration.\n"
|
|
432
|
+
+ json.dumps(perm.constraints, indent=4)
|
|
433
|
+
)
|
|
434
|
+
|
|
415
435
|
if warnings:
|
|
416
436
|
msg = """
|
|
417
437
|
One or more permission constraints may be affected by the Nautobot 2.0 migration.
|
|
@@ -5,6 +5,11 @@ from django.core.management import call_command
|
|
|
5
5
|
from django.core.management.base import CommandError
|
|
6
6
|
from django.contrib.contenttypes.models import ContentType
|
|
7
7
|
|
|
8
|
+
from nautobot.dcim.models.devices import Device
|
|
9
|
+
from nautobot.dcim.models.sites import Site
|
|
10
|
+
from nautobot.extras.choices import CustomFieldTypeChoices
|
|
11
|
+
from nautobot.extras.models.customfields import CustomField
|
|
12
|
+
from nautobot.users.models import ObjectPermission
|
|
8
13
|
from nautobot.utilities.testing import TestCase
|
|
9
14
|
from nautobot.dcim.models import VirtualChassis
|
|
10
15
|
from nautobot.extras.datasources.registry import get_datasource_contents
|
|
@@ -25,6 +30,17 @@ class PreMigrateCommandTest(TestCase):
|
|
|
25
30
|
)
|
|
26
31
|
self.git_repo.save(trigger_resync=False)
|
|
27
32
|
|
|
33
|
+
# Adding this test data to assert that https://github.com/nautobot/nautobot/issues/6081 is fixed
|
|
34
|
+
ct = ContentType.objects.get_for_model(GitRepository)
|
|
35
|
+
custom_field = CustomField.objects.create(type=CustomFieldTypeChoices.TYPE_TEXT, name="test_custom_field")
|
|
36
|
+
custom_field.content_types.add(ct)
|
|
37
|
+
self.obj_perm = ObjectPermission.objects.create(
|
|
38
|
+
name="Test permission",
|
|
39
|
+
constraints={"_custom_field_data__test_custom_field__in": ["test-1", "test-2"]},
|
|
40
|
+
actions=["view"],
|
|
41
|
+
)
|
|
42
|
+
self.obj_perm.object_types.add(ct)
|
|
43
|
+
|
|
28
44
|
def run_command(self, *args):
|
|
29
45
|
out = StringIO()
|
|
30
46
|
err = StringIO()
|
|
@@ -42,6 +58,8 @@ class PreMigrateCommandTest(TestCase):
|
|
|
42
58
|
out, err = self.run_command()
|
|
43
59
|
|
|
44
60
|
self.assertIn("All pre-migration checks passed.", out)
|
|
61
|
+
# Assert Permission constrain warning not logged
|
|
62
|
+
self.assertNotIn(f"ObjectPermission '{self.obj_perm.name}'", out)
|
|
45
63
|
self.assertEqual("", err)
|
|
46
64
|
|
|
47
65
|
def test_configcontext_failure(self):
|
|
@@ -80,3 +98,22 @@ class PreMigrateCommandTest(TestCase):
|
|
|
80
98
|
|
|
81
99
|
with self.assertRaises(CommandError):
|
|
82
100
|
self.run_command()
|
|
101
|
+
|
|
102
|
+
def test_permission_constraints_failure(self):
|
|
103
|
+
"""Test permission constraints logs warning when needed for CustomField."""
|
|
104
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
105
|
+
site_ct = ContentType.objects.get_for_model(Site)
|
|
106
|
+
custom_field = CustomField.objects.create(type=CustomFieldTypeChoices.TYPE_TEXT, name="custom_field")
|
|
107
|
+
custom_field.content_types.add(site_ct)
|
|
108
|
+
obj_perm = ObjectPermission.objects.create(
|
|
109
|
+
name="Test permission 2",
|
|
110
|
+
constraints={"site___custom_field_data__custom_field__in": ["test-1", "test-2"]},
|
|
111
|
+
actions=["view"],
|
|
112
|
+
)
|
|
113
|
+
obj_perm.object_types.add(device_ct)
|
|
114
|
+
|
|
115
|
+
out, _ = self.run_command()
|
|
116
|
+
self.assertIn(
|
|
117
|
+
f"ObjectPermission 'Test permission 2' (id: {obj_perm.pk}) has a constraint that references a model (nautobot.dcim.models.sites.Site) that will be migrated to a new model by the Nautobot 2.0 migration.",
|
|
118
|
+
out,
|
|
119
|
+
)
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -842,9 +842,16 @@ class Device(PrimaryModel, ConfigContextModel, StatusModel):
|
|
|
842
842
|
# Update Site and Rack assignment for any child Devices
|
|
843
843
|
devices = Device.objects.filter(parent_bay__device=self)
|
|
844
844
|
for device in devices:
|
|
845
|
-
|
|
846
|
-
device.
|
|
847
|
-
|
|
845
|
+
save_child_device = False
|
|
846
|
+
if device.site != self.site:
|
|
847
|
+
device.site = self.site
|
|
848
|
+
save_child_device = True
|
|
849
|
+
if device.rack != self.rack:
|
|
850
|
+
device.rack = self.rack
|
|
851
|
+
save_child_device = True
|
|
852
|
+
|
|
853
|
+
if save_child_device:
|
|
854
|
+
device.save()
|
|
848
855
|
|
|
849
856
|
def create_components(self):
|
|
850
857
|
"""Create device components from the device type definition."""
|
|
@@ -14,6 +14,7 @@ from nautobot.dcim.choices import (
|
|
|
14
14
|
InterfaceTypeChoices,
|
|
15
15
|
PortTypeChoices,
|
|
16
16
|
PowerOutletFeedLegChoices,
|
|
17
|
+
SubdeviceRoleChoices,
|
|
17
18
|
)
|
|
18
19
|
from nautobot.dcim.models import (
|
|
19
20
|
Cable,
|
|
@@ -925,6 +926,14 @@ class DeviceTestCase(TestCase):
|
|
|
925
926
|
manufacturer=manufacturer,
|
|
926
927
|
model="Test Device Type 1",
|
|
927
928
|
slug="test-device-type-1",
|
|
929
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
|
|
930
|
+
)
|
|
931
|
+
self.child_devicetype = DeviceType.objects.create(
|
|
932
|
+
model="Child Device Type 1",
|
|
933
|
+
slug="child-device-type-1",
|
|
934
|
+
manufacturer=manufacturer,
|
|
935
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
|
|
936
|
+
u_height=0,
|
|
928
937
|
)
|
|
929
938
|
self.device_role = DeviceRole.objects.create(
|
|
930
939
|
name="Test Device Role 1", slug="test-device-role-1", color="ff0000"
|
|
@@ -1164,6 +1173,71 @@ class DeviceTestCase(TestCase):
|
|
|
1164
1173
|
):
|
|
1165
1174
|
d1.validated_save()
|
|
1166
1175
|
|
|
1176
|
+
def test_child_devices_are_not_saved_when_unnecessary(self):
|
|
1177
|
+
parent_device = Device.objects.create(
|
|
1178
|
+
name="Parent Device 1",
|
|
1179
|
+
site=self.site,
|
|
1180
|
+
device_type=self.device_type,
|
|
1181
|
+
device_role=self.device_role,
|
|
1182
|
+
status=self.device_status,
|
|
1183
|
+
)
|
|
1184
|
+
parent_device.validated_save()
|
|
1185
|
+
|
|
1186
|
+
child_device = Device.objects.create(
|
|
1187
|
+
name="Child Device 1",
|
|
1188
|
+
site=parent_device.site,
|
|
1189
|
+
device_type=self.child_devicetype,
|
|
1190
|
+
device_role=parent_device.device_role,
|
|
1191
|
+
status=self.device_status,
|
|
1192
|
+
)
|
|
1193
|
+
child_device.validated_save()
|
|
1194
|
+
child_mtime_before_parent_saved = str(child_device.last_updated)
|
|
1195
|
+
|
|
1196
|
+
devicebay = DeviceBay.objects.get(device=parent_device, name="Device Bay 1")
|
|
1197
|
+
devicebay.installed_device = child_device
|
|
1198
|
+
devicebay.validated_save()
|
|
1199
|
+
|
|
1200
|
+
#
|
|
1201
|
+
# Tests
|
|
1202
|
+
#
|
|
1203
|
+
|
|
1204
|
+
#
|
|
1205
|
+
# On a NOOP save, the child device shouldn't be updated
|
|
1206
|
+
parent_device.save()
|
|
1207
|
+
|
|
1208
|
+
child_mtime_after_parent_noop_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1209
|
+
|
|
1210
|
+
self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_noop_save)
|
|
1211
|
+
|
|
1212
|
+
#
|
|
1213
|
+
# On a serial number update, the child device shouldn't be updated
|
|
1214
|
+
parent_device.serial = "12345"
|
|
1215
|
+
parent_device.save()
|
|
1216
|
+
|
|
1217
|
+
child_mtime_after_parent_serial_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1218
|
+
|
|
1219
|
+
self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_serial_update_save)
|
|
1220
|
+
|
|
1221
|
+
#
|
|
1222
|
+
# If the parent rack updates, the child mtime should update.
|
|
1223
|
+
rack = Rack.objects.create(name="Rack 1", site=parent_device.site)
|
|
1224
|
+
parent_device.rack = rack
|
|
1225
|
+
parent_device.save()
|
|
1226
|
+
|
|
1227
|
+
child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1228
|
+
|
|
1229
|
+
self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
|
|
1230
|
+
|
|
1231
|
+
#
|
|
1232
|
+
# If the parent site updates, the child mtime should update
|
|
1233
|
+
site = Site.objects.create(name="New Site 1")
|
|
1234
|
+
parent_device.site = site
|
|
1235
|
+
parent_device.save()
|
|
1236
|
+
|
|
1237
|
+
child_mtime_after_parent_site_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1238
|
+
|
|
1239
|
+
self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
|
|
1240
|
+
|
|
1167
1241
|
|
|
1168
1242
|
class CableTestCase(TestCase):
|
|
1169
1243
|
def setUp(self):
|