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.
- nautobot/core/api/mixins.py +10 -0
- nautobot/core/celery/encoders.py +2 -2
- nautobot/core/forms/fields.py +21 -5
- nautobot/core/forms/utils.py +1 -0
- nautobot/core/jobs/bulk_actions.py +1 -1
- nautobot/core/management/commands/generate_test_data.py +1 -1
- nautobot/core/models/name_color_content_types.py +9 -0
- nautobot/core/models/validators.py +7 -0
- nautobot/core/settings.py +0 -14
- nautobot/core/settings.yaml +0 -28
- nautobot/core/tables.py +6 -1
- nautobot/core/templates/generic/object_retrieve.html +1 -1
- nautobot/core/testing/api.py +18 -0
- nautobot/core/tests/nautobot_config.py +0 -2
- nautobot/core/tests/runner.py +17 -140
- nautobot/core/tests/test_api.py +4 -4
- nautobot/core/tests/test_authentication.py +83 -4
- nautobot/core/tests/test_forms.py +11 -8
- nautobot/core/tests/test_graphql.py +9 -0
- nautobot/core/tests/test_jobs.py +7 -0
- nautobot/core/ui/object_detail.py +31 -0
- nautobot/dcim/factory.py +2 -0
- nautobot/dcim/filters/__init__.py +5 -0
- nautobot/dcim/forms.py +17 -1
- nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
- nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
- nautobot/dcim/models/devices.py +9 -2
- nautobot/dcim/tables/devices.py +1 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
- nautobot/dcim/tests/test_api.py +74 -31
- nautobot/dcim/tests/test_filters.py +2 -0
- nautobot/dcim/tests/test_models.py +65 -0
- nautobot/dcim/tests/test_views.py +3 -0
- nautobot/extras/forms/forms.py +7 -3
- nautobot/extras/plugins/marketplace_manifest.yml +18 -0
- nautobot/extras/tables.py +4 -5
- nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/status.html +1 -37
- nautobot/extras/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/test_api.py +22 -7
- nautobot/extras/tests/test_changelog.py +4 -4
- nautobot/extras/tests/test_customfields.py +3 -0
- nautobot/extras/tests/test_plugins.py +19 -13
- nautobot/extras/tests/test_relationships.py +9 -0
- nautobot/extras/tests/test_tags.py +2 -2
- nautobot/extras/tests/test_views.py +15 -6
- nautobot/extras/urls.py +1 -30
- nautobot/extras/views.py +10 -54
- nautobot/ipam/tables.py +6 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
- nautobot/ipam/templates/ipam/service.html +2 -46
- nautobot/ipam/templates/ipam/service_edit.html +1 -17
- nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
- nautobot/ipam/tests/migration/__init__.py +0 -0
- nautobot/ipam/tests/migration/test_migrations.py +510 -0
- nautobot/ipam/tests/test_api.py +66 -36
- nautobot/ipam/tests/test_filters.py +0 -10
- nautobot/ipam/tests/test_views.py +44 -2
- nautobot/ipam/urls.py +2 -47
- nautobot/ipam/utils/migrations.py +185 -152
- nautobot/ipam/utils/testing.py +177 -0
- nautobot/ipam/views.py +95 -157
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
- nautobot/project-static/docs/development/apps/api/testing.html +0 -87
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +78 -107
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +24 -198
- nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.4.html +226 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +89 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
- nautobot/virtualization/forms.py +20 -0
- nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
- nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
- nautobot/virtualization/tests/test_api.py +14 -3
- nautobot/virtualization/tests/test_views.py +10 -2
- nautobot/virtualization/urls.py +10 -93
- nautobot/virtualization/views.py +33 -72
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/METADATA +6 -5
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/RECORD +113 -108
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
- nautobot/core/tests/performance_baselines.yml +0 -8900
- nautobot/ipam/tests/test_migrations.py +0 -462
- /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
- {nautobot-2.4.5.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))
|