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,510 @@
1
+ import uuid
2
+
3
+ from django_test_migrations.contrib.unittest_case import MigratorTestCase
4
+ import netaddr
5
+
6
+ from nautobot.core.models.utils import serialize_object
7
+ from nautobot.extras import choices as extras_choices
8
+ from nautobot.ipam.utils.testing import create_prefixes_and_ips
9
+
10
+
11
+ class AggregateToPrefixMigrationTestCase(MigratorTestCase):
12
+ """Test data migrations removing the Aggregate model and replacing with Prefix in v2.0"""
13
+
14
+ migrate_from = ("ipam", "0021_prefix_add_rir_and_date_allocated")
15
+ migrate_to = ("ipam", "0022_aggregate_to_prefix_data_migration")
16
+
17
+ def _create_objectchange(self, instance, change_context_detail):
18
+ ContentType = self.old_state.apps.get_model("contenttypes", "contenttype")
19
+ ObjectChange = self.old_state.apps.get_model("extras", "objectchange")
20
+
21
+ instance.refresh_from_db()
22
+ return ObjectChange.objects.create(
23
+ action=extras_choices.ObjectChangeActionChoices.ACTION_UPDATE,
24
+ change_context=extras_choices.ObjectChangeEventContextChoices.CONTEXT_ORM,
25
+ change_context_detail=change_context_detail,
26
+ changed_object_id=instance.pk,
27
+ changed_object_type=ContentType.objects.get_for_model(instance.__class__),
28
+ object_data=serialize_object(instance),
29
+ object_repr="",
30
+ request_id=uuid.uuid4(),
31
+ )
32
+
33
+ def prepare(self):
34
+ """Populate Aggregate data before migrating to Prefixes"""
35
+
36
+ Aggregate = self.old_state.apps.get_model("ipam", "Aggregate")
37
+ ContentType = self.old_state.apps.get_model("contenttypes", "ContentType")
38
+ CustomField = self.old_state.apps.get_model("extras", "customfield")
39
+ Note = self.old_state.apps.get_model("extras", "note")
40
+ ObjectPermission = self.old_state.apps.get_model("users", "objectpermission")
41
+ Prefix = self.old_state.apps.get_model("ipam", "prefix")
42
+ RIR = self.old_state.apps.get_model("ipam", "RIR")
43
+ Status = self.old_state.apps.get_model("extras", "status")
44
+ Tag = self.old_state.apps.get_model("extras", "tag")
45
+ TaggedItem = self.old_state.apps.get_model("extras", "TaggedItem")
46
+
47
+ self.aggregate_ct = ContentType.objects.get_for_model(Aggregate)
48
+ self.prefix_ct = ContentType.objects.get_for_model(Prefix)
49
+
50
+ self.prefix_status, _ = Status.objects.get_or_create(name="Active")
51
+ self.prefix_status.content_types.add(self.prefix_ct)
52
+
53
+ self.rir1 = RIR.objects.create(name="RFC1918", is_private=True)
54
+ self.rir2 = RIR.objects.create(name="ARIN")
55
+
56
+ # Create 4 prefixes that will be merged into by Aggregates with duplicate network/prefix_length
57
+ self.prefix1 = Prefix.objects.create(
58
+ network="10.1.0.0",
59
+ broadcast="10.1.0.255",
60
+ prefix_length=24,
61
+ status=self.prefix_status,
62
+ description="PrefixDesc",
63
+ )
64
+ self.prefix2 = Prefix.objects.create(
65
+ network="10.2.0.0",
66
+ broadcast="10.2.0.127",
67
+ prefix_length=25,
68
+ status=self.prefix_status,
69
+ description="PrefixDesc",
70
+ )
71
+ self.prefix3 = Prefix.objects.create(
72
+ network="10.3.0.0", broadcast="10.3.0.63", prefix_length=26, status=self.prefix_status
73
+ )
74
+ self.prefix4 = Prefix.objects.create(
75
+ network="10.4.0.0", broadcast="10.4.0.31", prefix_length=27, status=self.prefix_status
76
+ )
77
+ self.aggregate1 = Aggregate.objects.create(
78
+ network="10.1.0.0", broadcast="10.1.0.255", rir=self.rir1, prefix_length=24
79
+ )
80
+ self.aggregate2 = Aggregate.objects.create(
81
+ network="10.2.0.0", broadcast="10.2.0.127", rir=self.rir1, prefix_length=25
82
+ )
83
+ self.aggregate3 = Aggregate.objects.create(
84
+ network="10.3.0.0", broadcast="10.3.0.63", rir=self.rir1, prefix_length=26, description="AggregateDesc"
85
+ )
86
+ self.aggregate4 = Aggregate.objects.create(
87
+ network="10.4.0.0", broadcast="10.4.0.31", rir=self.rir1, prefix_length=27, description="AggregateDesc"
88
+ )
89
+
90
+ # Create 8 prefixes that are not duplicated by Aggregates and will not be touched by migration
91
+ # self.prefix5(10.5.0.0)
92
+ # ...
93
+ # self.prefix12(10.12.0.0)
94
+ for i in range(8):
95
+ prefix = Prefix.objects.create(
96
+ network=f"10.{i+5}.0.0", broadcast=f"10.{i+5}.0.15", prefix_length=28, status=self.prefix_status
97
+ )
98
+ setattr(self, f"prefix{i+5}", prefix)
99
+
100
+ # Create 16 aggregates that will be migrated to new Prefixes
101
+ # self.aggregate5(8.5.0.0)
102
+ # ...
103
+ # self.aggregate20(8.20.0.0)
104
+ for i in range(16):
105
+ aggregate = Aggregate.objects.create(
106
+ network=f"8.{i+5}.0.0",
107
+ broadcast=f"8.{i+5}.0.7",
108
+ rir=self.rir2,
109
+ prefix_length=29,
110
+ description="AggregateDesc",
111
+ )
112
+ setattr(self, f"aggregate{i+5}", aggregate)
113
+
114
+ # tags
115
+ self.prefix_tag_a = Tag.objects.create(name="PrefixTagA", slug="prefixtaga")
116
+ self.prefix_tag_b = Tag.objects.create(name="PrefixTagB", slug="prefixtagb")
117
+ self.prefix_tag_a.content_types.add(self.prefix_ct)
118
+ self.prefix_tag_b.content_types.add(self.prefix_ct)
119
+ self.aggregate_tag_a = Tag.objects.create(name="AggregateTagA", slug="aggregatetaga")
120
+ self.aggregate_tag_b = Tag.objects.create(name="AggregateTagB", slug="aggregatetagb")
121
+ self.aggregate_tag_a.content_types.add(self.aggregate_ct)
122
+ self.aggregate_tag_b.content_types.add(self.aggregate_ct)
123
+ TaggedItem.objects.create(tag=self.prefix_tag_a, content_type=self.prefix_ct, object_id=self.prefix1.id)
124
+ TaggedItem.objects.create(tag=self.prefix_tag_a, content_type=self.prefix_ct, object_id=self.prefix2.id)
125
+ TaggedItem.objects.create(tag=self.prefix_tag_a, content_type=self.prefix_ct, object_id=self.prefix3.id)
126
+ TaggedItem.objects.create(tag=self.prefix_tag_a, content_type=self.prefix_ct, object_id=self.prefix4.id)
127
+ TaggedItem.objects.create(
128
+ tag=self.aggregate_tag_a, content_type=self.aggregate_ct, object_id=self.aggregate1.id
129
+ )
130
+ TaggedItem.objects.create(
131
+ tag=self.aggregate_tag_b, content_type=self.aggregate_ct, object_id=self.aggregate1.id
132
+ )
133
+ TaggedItem.objects.create(
134
+ tag=self.aggregate_tag_a, content_type=self.aggregate_ct, object_id=self.aggregate2.id
135
+ )
136
+ TaggedItem.objects.create(
137
+ tag=self.aggregate_tag_b, content_type=self.aggregate_ct, object_id=self.aggregate3.id
138
+ )
139
+ TaggedItem.objects.create(
140
+ tag=self.aggregate_tag_a,
141
+ content_type=self.aggregate_ct,
142
+ object_id=self.aggregate5.id, # pylint: disable=no-member
143
+ )
144
+ TaggedItem.objects.create(
145
+ tag=self.aggregate_tag_b,
146
+ content_type=self.aggregate_ct,
147
+ object_id=self.aggregate5.id, # pylint: disable=no-member
148
+ )
149
+ TaggedItem.objects.create(
150
+ tag=self.aggregate_tag_b,
151
+ content_type=self.aggregate_ct,
152
+ object_id=self.aggregate6.id, # pylint: disable=no-member
153
+ )
154
+
155
+ # notes
156
+ Note.objects.create(
157
+ note="Prefix1 test note",
158
+ assigned_object_type=self.prefix_ct,
159
+ assigned_object_id=self.prefix1.id,
160
+ )
161
+ Note.objects.create(
162
+ note="Prefix2 test note",
163
+ assigned_object_type=self.prefix_ct,
164
+ assigned_object_id=self.prefix2.id,
165
+ )
166
+ Note.objects.create(
167
+ note="Aggregate1 test note",
168
+ assigned_object_type=self.aggregate_ct,
169
+ assigned_object_id=self.aggregate1.id,
170
+ )
171
+ Note.objects.create(
172
+ note="Aggregate3 test note",
173
+ assigned_object_type=self.aggregate_ct,
174
+ assigned_object_id=self.aggregate3.id,
175
+ )
176
+ Note.objects.create(
177
+ note="Aggregate5 test note",
178
+ assigned_object_type=self.aggregate_ct,
179
+ assigned_object_id=self.aggregate5.id, # pylint: disable=no-member
180
+ )
181
+
182
+ # object permissions
183
+ object_permission1 = ObjectPermission.objects.create(
184
+ name="Aggregate permission 1", actions=["view", "add", "change", "delete"]
185
+ )
186
+ object_permission2 = ObjectPermission.objects.create(
187
+ name="Aggregate permission 2", actions=["add", "delete"], enabled=False
188
+ )
189
+ object_permission1.object_types.add(self.aggregate_ct)
190
+ object_permission2.object_types.add(self.aggregate_ct)
191
+
192
+ # object changes
193
+ self._create_objectchange(self.prefix1, "Pre-migration object change for prefix1")
194
+ self._create_objectchange(self.prefix4, "Pre-migration object change for prefix4")
195
+ self._create_objectchange(self.prefix5, "Pre-migration object change for prefix5") # pylint: disable=no-member
196
+ self._create_objectchange(self.aggregate5, "Pre-migration object change for aggregate5") # pylint: disable=no-member
197
+
198
+ # custom fields
199
+ prefix_cf1 = CustomField.objects.create(name="prefixcf1", slug="prefixcf1")
200
+ prefix_cf1.content_types.add(self.prefix_ct)
201
+ aggregate_cf1 = CustomField.objects.create(name="aggregatecf1", slug="aggregatecf1")
202
+ aggregate_cf1.content_types.add(self.aggregate_ct)
203
+ prefixaggregate_cf1 = CustomField.objects.create(name="prefixaggregatecf1", slug="prefixaggregatecf1")
204
+ prefixaggregate_cf1.content_types.add(self.aggregate_ct, self.prefix_ct)
205
+
206
+ self.prefix1._custom_field_data["prefixcf1"] = "testdata prefixcf1 prefix1"
207
+ self.prefix1._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 prefix1"
208
+ self.aggregate1._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate1"
209
+
210
+ self.prefix2._custom_field_data["prefixcf1"] = "testdata prefixcf1 prefix2"
211
+ self.prefix2._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 prefix2"
212
+ self.aggregate2._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate2"
213
+ self.aggregate2._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate2"
214
+
215
+ self.aggregate3._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate3"
216
+
217
+ self.prefix5._custom_field_data["prefixcf1"] = "testdata prefixcf1 prefix5" # pylint: disable=no-member
218
+ self.prefix5._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 prefix5" # pylint: disable=no-member
219
+
220
+ self.aggregate5._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate5" # pylint: disable=no-member
221
+ self.aggregate5._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate5" # pylint: disable=no-member
222
+
223
+ self.aggregate6._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate6" # pylint: disable=no-member
224
+
225
+ self.prefix1.save()
226
+ self.prefix2.save()
227
+ self.prefix3.save()
228
+ self.prefix4.save()
229
+ self.prefix5.save() # pylint: disable=no-member
230
+ self.aggregate1.save()
231
+ self.aggregate2.save()
232
+ self.aggregate3.save()
233
+ self.aggregate5.save() # pylint: disable=no-member
234
+ self.aggregate6.save() # pylint: disable=no-member
235
+
236
+ def test_validate_data(self):
237
+ Aggregate = self.new_state.apps.get_model("ipam", "Aggregate")
238
+ ContentType = self.new_state.apps.get_model("contenttypes", "ContentType")
239
+ CustomField = self.new_state.apps.get_model("extras", "customfield")
240
+ Note = self.new_state.apps.get_model("extras", "Note")
241
+ ObjectChange = self.new_state.apps.get_model("extras", "objectchange")
242
+ ObjectPermission = self.new_state.apps.get_model("users", "objectpermission")
243
+ Prefix = self.new_state.apps.get_model("ipam", "Prefix")
244
+ Tag = self.new_state.apps.get_model("extras", "Tag")
245
+ TaggedItem = self.new_state.apps.get_model("extras", "TaggedItem")
246
+
247
+ prefix_ct = ContentType.objects.get_for_model(Prefix)
248
+
249
+ with self.subTest("object count"):
250
+ with self.subTest("Test Prefix count"):
251
+ self.assertEqual(Prefix.objects.count(), 28)
252
+ with self.subTest("Test Aggregate count"):
253
+ self.assertEqual(Aggregate.objects.count(), 20)
254
+
255
+ with self.subTest("network"):
256
+ for i in range(16):
257
+ self.assertTrue(
258
+ Prefix.objects.filter(network=f"8.{i+5}.0.0", prefix_length=29, rir_id=self.rir2.id).exists()
259
+ )
260
+
261
+ with self.subTest("rir"):
262
+ with self.subTest(f"prefix.rir = {self.rir1.name}"):
263
+ self.assertEqual(Prefix.objects.get(network="10.1.0.0").rir_id, self.rir1.id)
264
+ self.assertEqual(Prefix.objects.get(network="10.2.0.0").rir_id, self.rir1.id)
265
+ self.assertEqual(Prefix.objects.get(network="10.3.0.0").rir_id, self.rir1.id)
266
+ self.assertEqual(Prefix.objects.get(network="10.4.0.0").rir_id, self.rir1.id)
267
+ with self.subTest(f"prefix.rir = {self.rir2.name}"):
268
+ for i in range(16):
269
+ prefix = Prefix.objects.get(network=f"8.{i+5}.0.0")
270
+ self.assertEqual(prefix.rir_id, self.rir2.id)
271
+ with self.subTest("prefix.rir is None"):
272
+ for i in range(8):
273
+ prefix = Prefix.objects.get(network=f"10.{i+5}.0.0")
274
+ self.assertIsNone(prefix.rir_id)
275
+
276
+ with self.subTest("description"):
277
+ self.assertEqual(Prefix.objects.filter(description="PrefixDesc").count(), 2)
278
+ self.assertEqual(Prefix.objects.filter(description="").count(), 10)
279
+ self.assertEqual(Prefix.objects.filter(description="AggregateDesc").count(), 16)
280
+
281
+ with self.subTest("status"):
282
+ self.assertEqual(Prefix.objects.filter(status__name="Active").count(), Prefix.objects.count())
283
+
284
+ with self.subTest("tags"):
285
+ with self.subTest("Prefix content type was added to Aggregate Tags"):
286
+ prefix_tags = Tag.objects.filter(content_types=ContentType.objects.get_for_model(Prefix)).values_list(
287
+ "name", flat=True
288
+ )
289
+ self.assertIn("AggregateTagA", prefix_tags)
290
+ self.assertIn("AggregateTagB", prefix_tags)
291
+
292
+ # assert that tags were migrated to new prefix instances
293
+ # compare list of PKs since tag managers don't work in migrations
294
+ prefix = Prefix.objects.get(network="10.1.0.0")
295
+ self.assertCountEqual(
296
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list("tag_id", flat=True),
297
+ Tag.objects.filter(name__in=["PrefixTagA", "AggregateTagA", "AggregateTagB"]).values_list(
298
+ "id", flat=True
299
+ ),
300
+ )
301
+ prefix = Prefix.objects.get(network="10.2.0.0")
302
+ self.assertCountEqual(
303
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list("tag_id", flat=True),
304
+ Tag.objects.filter(name__in=["PrefixTagA", "AggregateTagA"]).values_list("id", flat=True),
305
+ )
306
+ prefix = Prefix.objects.get(network="10.3.0.0")
307
+ self.assertCountEqual(
308
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list("tag_id", flat=True),
309
+ Tag.objects.filter(name__in=["PrefixTagA", "AggregateTagB"]).values_list("id", flat=True),
310
+ )
311
+ prefix = Prefix.objects.get(network="10.4.0.0")
312
+ self.assertCountEqual(
313
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list("tag_id", flat=True),
314
+ Tag.objects.filter(name="PrefixTagA").values_list("id", flat=True),
315
+ )
316
+ prefix = Prefix.objects.get(network="8.5.0.0")
317
+ self.assertCountEqual(
318
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list("tag_id", flat=True),
319
+ Tag.objects.filter(name__in=["AggregateTagA", "AggregateTagB"]).values_list("id", flat=True),
320
+ )
321
+ prefix = Prefix.objects.get(network="8.6.0.0")
322
+ self.assertCountEqual(
323
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list("tag_id", flat=True),
324
+ Tag.objects.filter(name="AggregateTagB").values_list("id", flat=True),
325
+ )
326
+ for i in range(7, 21):
327
+ prefix = Prefix.objects.get(network=f"8.{i}.0.0")
328
+ self.assertCountEqual(
329
+ TaggedItem.objects.filter(content_type=prefix_ct, object_id=prefix.id).values_list(
330
+ "tag_id", flat=True
331
+ ),
332
+ [],
333
+ )
334
+
335
+ with self.subTest("notes"):
336
+ # no notes are assigned to aggregates
337
+ self.assertQuerysetEqual(
338
+ Note.objects.filter(assigned_object_type=ContentType.objects.get_for_model(Aggregate)),
339
+ Note.objects.none(),
340
+ )
341
+ # no extra notes were created
342
+ self.assertEqual(Note.objects.count(), 5)
343
+
344
+ # aggregate1 note added on top of existing note on prefix1
345
+ self.assertQuerysetEqual(
346
+ Note.objects.filter(assigned_object_type=prefix_ct, assigned_object_id=self.prefix1.id),
347
+ Note.objects.filter(note__in=["Prefix1 test note", "Aggregate1 test note"]),
348
+ )
349
+
350
+ # prefix2 note was unchanged
351
+ self.assertQuerysetEqual(
352
+ Note.objects.filter(assigned_object_type=prefix_ct, assigned_object_id=self.prefix2.id),
353
+ Note.objects.filter(note="Prefix2 test note"),
354
+ )
355
+
356
+ # aggregate3 note was migrated to prefix3
357
+ self.assertQuerysetEqual(
358
+ Note.objects.filter(assigned_object_type=prefix_ct, assigned_object_id=self.prefix3.id),
359
+ Note.objects.filter(note="Aggregate3 test note"),
360
+ )
361
+
362
+ # no notes for prefix4
363
+ self.assertQuerysetEqual(
364
+ Note.objects.filter(assigned_object_type=prefix_ct, assigned_object_id=self.prefix4.id),
365
+ Note.objects.none(),
366
+ )
367
+
368
+ # aggregate5 note was migrated to new prefix object
369
+ aggregate5_migrated_prefix = Prefix.objects.get(network="8.5.0.0")
370
+ self.assertQuerysetEqual(
371
+ Note.objects.filter(
372
+ assigned_object_type=prefix_ct,
373
+ assigned_object_id=aggregate5_migrated_prefix.id,
374
+ ),
375
+ Note.objects.filter(note="Aggregate5 test note"),
376
+ )
377
+
378
+ # no other notes are related to remaining prefixes
379
+ for i in range(5, 13):
380
+ prefix = Prefix.objects.get(network=f"10.{i}.0.0")
381
+ self.assertQuerysetEqual(
382
+ Note.objects.filter(assigned_object_type=prefix_ct, assigned_object_id=prefix.id),
383
+ Note.objects.none(),
384
+ )
385
+ for i in range(6, 21):
386
+ prefix = Prefix.objects.get(network=f"8.{i}.0.0")
387
+ self.assertQuerysetEqual(
388
+ Note.objects.filter(assigned_object_type=prefix_ct, assigned_object_id=prefix.id),
389
+ Note.objects.none(),
390
+ )
391
+
392
+ with self.subTest("permissions"):
393
+ self.assertEqual(ObjectPermission.objects.count(), 2)
394
+
395
+ # assert prefix content type was added to object permission 1
396
+ object_permission1 = ObjectPermission.objects.filter(
397
+ name="Aggregate permission 1", actions=["view", "add", "change", "delete"]
398
+ )
399
+ self.assertTrue(object_permission1.exists())
400
+ self.assertTrue(object_permission1.first().object_types.filter(id=prefix_ct.id).exists())
401
+
402
+ # assert prefix content type was added to object permission 2
403
+ object_permission2 = ObjectPermission.objects.filter(
404
+ name="Aggregate permission 2", actions=["add", "delete"], enabled=False
405
+ )
406
+ self.assertTrue(object_permission2.exists())
407
+ self.assertTrue(object_permission2.first().object_types.filter(id=prefix_ct.id).exists())
408
+
409
+ with self.subTest("object changes"):
410
+ self.assertEqual(
411
+ ObjectChange.objects.filter(changed_object_type=ContentType.objects.get_for_model(Prefix)).count(), 24
412
+ )
413
+ self.assertEqual(
414
+ ObjectChange.objects.filter(changed_object_type=ContentType.objects.get_for_model(Aggregate)).count(), 0
415
+ )
416
+
417
+ for prefix in (self.prefix1, self.prefix4, Prefix.objects.get(network="8.5.0.0")):
418
+ self.assertEqual(
419
+ ObjectChange.objects.filter(changed_object_id=prefix.id).count(),
420
+ 2,
421
+ )
422
+
423
+ for prefix in (self.prefix2, self.prefix3, self.prefix5): # pylint: disable=no-member
424
+ self.assertEqual(
425
+ ObjectChange.objects.filter(changed_object_id=prefix.id).count(),
426
+ 1,
427
+ )
428
+
429
+ for i in range(6, 13):
430
+ prefix = Prefix.objects.get(network=f"10.{i}.0.0")
431
+ self.assertEqual(
432
+ ObjectChange.objects.filter(changed_object_id=prefix.id).count(),
433
+ 0,
434
+ )
435
+
436
+ for i in range(6, 21):
437
+ prefix = Prefix.objects.get(network=f"8.{i}.0.0")
438
+ self.assertEqual(
439
+ ObjectChange.objects.filter(changed_object_id=prefix.id).count(),
440
+ 1,
441
+ )
442
+
443
+ with self.subTest("custom fields"):
444
+ # This change is necessary because name attribute is not specified now in example_app's signal.py
445
+ self.assertEqual(CustomField.objects.exclude(name="").count(), 3)
446
+ self.assertEqual(
447
+ CustomField.objects.filter(content_types=ContentType.objects.get_for_model(Prefix)).count(), 3
448
+ )
449
+ self.assertEqual(
450
+ CustomField.objects.filter(content_types=ContentType.objects.get_for_model(Aggregate)).count(), 2
451
+ )
452
+
453
+ expected = {
454
+ "prefix1": {
455
+ "prefixcf1": "testdata prefixcf1 prefix1",
456
+ "prefixaggregatecf1": "testdata prefixaggregatecf1 prefix1",
457
+ "aggregatecf1": "testdata aggregatecf1 aggregate1",
458
+ },
459
+ "prefix2": {
460
+ "prefixcf1": "testdata prefixcf1 prefix2",
461
+ "prefixaggregatecf1": "testdata prefixaggregatecf1 prefix2",
462
+ "aggregatecf1": "testdata aggregatecf1 aggregate2",
463
+ },
464
+ "prefix3": {
465
+ "aggregatecf1": "testdata aggregatecf1 aggregate3",
466
+ },
467
+ "prefix4": {},
468
+ "prefix5": {
469
+ "prefixcf1": "testdata prefixcf1 prefix5",
470
+ "prefixaggregatecf1": "testdata prefixaggregatecf1 prefix5",
471
+ },
472
+ }
473
+
474
+ for i in range(1, 6):
475
+ with self.subTest(f"Custom fields for prefix{i}"):
476
+ prefix = Prefix.objects.get(network=f"10.{i}.0.0")
477
+ self.assertDictEqual(prefix._custom_field_data, expected[f"prefix{i}"])
478
+
479
+ with self.subTest("Custom fields for prefix 8.5.0.0"):
480
+ expected = {
481
+ "prefixaggregatecf1": "testdata prefixaggregatecf1 aggregate5",
482
+ "aggregatecf1": "testdata aggregatecf1 aggregate5",
483
+ }
484
+ prefix = Prefix.objects.get(network="8.5.0.0")
485
+ self.assertDictEqual(prefix._custom_field_data, expected)
486
+
487
+ with self.subTest("Custom fields for prefix 8.6.0.0"):
488
+ expected = {
489
+ "prefixaggregatecf1": "testdata prefixaggregatecf1 aggregate6",
490
+ }
491
+ prefix = Prefix.objects.get(network="8.6.0.0")
492
+ self.assertDictEqual(prefix._custom_field_data, expected)
493
+
494
+
495
+ class IPAMDataMigration0031TestCase(MigratorTestCase):
496
+ migrate_from = ("ipam", "0030_ipam__namespaces")
497
+ migrate_to = ("ipam", "0032_ipam__namespaces_finish")
498
+
499
+ def prepare(self):
500
+ # Create an arbitrary set of prefixes and IPs mostly subdividing and consuming the given subnet, including dupes
501
+ create_prefixes_and_ips("10.0.0.0/14", apps=self.old_state.apps)
502
+
503
+ def test_validate_data(self):
504
+ IPAddress = self.new_state.apps.get_model("ipam", "IPAddress")
505
+
506
+ with self.subTest("Verify that all IPAddresses now have a valid parent"):
507
+ self.assertQuerysetEqual(IPAddress.objects.filter(parent__isnull=True), IPAddress.objects.none())
508
+ for ip in IPAddress.objects.iterator():
509
+ self.assertLessEqual(netaddr.IPAddress(ip.parent.network), netaddr.IPAddress(ip.host))
510
+ self.assertGreaterEqual(netaddr.IPAddress(ip.parent.broadcast), netaddr.IPAddress(ip.host))