nautobot 2.3.7__py3-none-any.whl → 2.3.8__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.
- nautobot/core/templates/inc/paginator.html +3 -0
- nautobot/core/templates/utilities/obj_table.html +1 -1
- nautobot/dcim/api/serializers.py +10 -5
- nautobot/dcim/forms.py +11 -7
- nautobot/dcim/models/device_components.py +7 -4
- nautobot/dcim/tests/test_api.py +28 -0
- nautobot/dcim/tests/test_forms.py +17 -1
- nautobot/dcim/tests/test_models.py +42 -4
- nautobot/dcim/utils.py +9 -6
- nautobot/extras/plugins/views.py +18 -3
- nautobot/ipam/filters.py +2 -2
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/templates/ipam/ipaddress.html +2 -2
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/prefix.html +3 -3
- nautobot/ipam/templates/ipam/routetarget.html +2 -2
- nautobot/ipam/templates/ipam/vlan.html +3 -0
- nautobot/ipam/templates/ipam/vrf.html +7 -4
- nautobot/ipam/tests/test_models.py +68 -12
- nautobot/ipam/views.py +43 -0
- nautobot/project-static/docs/release-notes/version-2.3.html +86 -29
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +269 -269
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.8.dist-info}/METADATA +1 -1
- {nautobot-2.3.7.dist-info → nautobot-2.3.8.dist-info}/RECORD +31 -31
- {nautobot-2.3.7.dist-info → nautobot-2.3.8.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.8.dist-info}/NOTICE +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.8.dist-info}/WHEEL +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.8.dist-info}/entry_points.txt +0 -0
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
{% endif %}
|
|
30
30
|
{% endfor %}
|
|
31
31
|
<select name="per_page" id="per_page" class="form-control">
|
|
32
|
+
{% if page.paginator.per_page not in "PER_PAGE_DEFAULTS"|settings_or_config %}
|
|
33
|
+
<option value="{{ page.paginator.per_page }}" selected="selected">{{ page.paginator.per_page }}</option>
|
|
34
|
+
{% endif %}
|
|
32
35
|
{% for n in "PER_PAGE_DEFAULTS"|settings_or_config %}
|
|
33
36
|
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
|
|
34
37
|
{% endfor %}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{% else %}
|
|
46
46
|
{% include table_template|default:'responsive_table.html' %}
|
|
47
47
|
{% endif %}
|
|
48
|
-
{% if not disable_pagination %}
|
|
48
|
+
{% if table.paginator.num_pages > 1 and not disable_pagination %}
|
|
49
49
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
|
50
50
|
{% endif %}
|
|
51
51
|
<div class="clearfix"></div>
|
nautobot/dcim/api/serializers.py
CHANGED
|
@@ -798,14 +798,19 @@ class InterfaceSerializer(
|
|
|
798
798
|
def validate(self, data):
|
|
799
799
|
# Validate many-to-many VLAN assignments
|
|
800
800
|
device = self.instance.device if self.instance else data.get("device")
|
|
801
|
-
|
|
802
|
-
|
|
801
|
+
location = None
|
|
802
|
+
if device:
|
|
803
|
+
location = device.location
|
|
804
|
+
if location:
|
|
805
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
806
|
+
else:
|
|
807
|
+
location_ids = []
|
|
803
808
|
for vlan in data.get("tagged_vlans", []):
|
|
804
|
-
if vlan.locations.exists() and not vlan.locations.filter(
|
|
809
|
+
if vlan.locations.exists() and not vlan.locations.filter(pk__in=location_ids).exists():
|
|
805
810
|
raise serializers.ValidationError(
|
|
806
811
|
{
|
|
807
|
-
"tagged_vlans": f"VLAN {vlan} must have
|
|
808
|
-
f"it must be global."
|
|
812
|
+
"tagged_vlans": f"VLAN {vlan} must have the same location as the interface's parent device, "
|
|
813
|
+
f"or is in one of the parents of the interface's parent device's location, or it must be global."
|
|
809
814
|
}
|
|
810
815
|
)
|
|
811
816
|
|
nautobot/dcim/forms.py
CHANGED
|
@@ -213,23 +213,27 @@ class InterfaceCommonForm(forms.Form):
|
|
|
213
213
|
elif mode == InterfaceModeChoices.MODE_TAGGED_ALL:
|
|
214
214
|
self.cleaned_data["tagged_vlans"] = []
|
|
215
215
|
|
|
216
|
-
# Validate tagged VLANs; must be a global VLAN or in the same location
|
|
217
|
-
#
|
|
218
|
-
# belongs to the parent Location or the child location of the parent device to the `tagged_vlan` field of the interface?
|
|
216
|
+
# Validate tagged VLANs; must be a global VLAN or in the same location as the
|
|
217
|
+
# parent device/VM or any of that location's parent locations
|
|
219
218
|
elif mode == InterfaceModeChoices.MODE_TAGGED:
|
|
220
|
-
|
|
219
|
+
location = self.cleaned_data[parent_field].location
|
|
220
|
+
if location:
|
|
221
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
222
|
+
else:
|
|
223
|
+
location_ids = []
|
|
221
224
|
invalid_vlans = [
|
|
222
225
|
str(v)
|
|
223
226
|
for v in tagged_vlans
|
|
224
227
|
if v.locations.without_tree_fields().exists()
|
|
225
|
-
and not VLANLocationAssignment.objects.filter(
|
|
228
|
+
and not VLANLocationAssignment.objects.filter(location__in=location_ids, vlan=v).exists()
|
|
226
229
|
]
|
|
227
230
|
|
|
228
231
|
if invalid_vlans:
|
|
229
232
|
raise forms.ValidationError(
|
|
230
233
|
{
|
|
231
|
-
"tagged_vlans": f"The tagged VLANs ({', '.join(invalid_vlans)}) must
|
|
232
|
-
|
|
234
|
+
"tagged_vlans": f"The tagged VLANs ({', '.join(invalid_vlans)}) must have the same location as the "
|
|
235
|
+
"interface's parent device, or is in one of the parents of the interface's parent device's location, "
|
|
236
|
+
"or it must be global."
|
|
233
237
|
}
|
|
234
238
|
)
|
|
235
239
|
|
|
@@ -727,19 +727,22 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
|
|
|
727
727
|
)
|
|
728
728
|
|
|
729
729
|
# Validate untagged VLAN
|
|
730
|
-
|
|
731
|
-
|
|
730
|
+
location = self.parent.location if self.parent is not None else None
|
|
731
|
+
if location:
|
|
732
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
733
|
+
else:
|
|
734
|
+
location_ids = []
|
|
732
735
|
if (
|
|
733
736
|
self.untagged_vlan
|
|
734
737
|
and self.untagged_vlan.locations.exists()
|
|
735
738
|
and self.parent
|
|
736
|
-
and not self.untagged_vlan.locations.filter(
|
|
739
|
+
and not self.untagged_vlan.locations.filter(pk__in=location_ids).exists()
|
|
737
740
|
):
|
|
738
741
|
raise ValidationError(
|
|
739
742
|
{
|
|
740
743
|
"untagged_vlan": (
|
|
741
744
|
f"The untagged VLAN ({self.untagged_vlan}) must have a common location as the interface's parent "
|
|
742
|
-
f"device, or it must be global."
|
|
745
|
+
f"device, or is in one of the parents of the interface's parent device's location, or it must be global."
|
|
743
746
|
)
|
|
744
747
|
}
|
|
745
748
|
)
|
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -2198,6 +2198,34 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2198
2198
|
self.client.post(url, self.untagged_vlan_data, format="json", **self.header), status.HTTP_201_CREATED
|
|
2199
2199
|
)
|
|
2200
2200
|
|
|
2201
|
+
def test_tagged_vlan_must_be_in_the_location_or_parent_locations_of_the_parent_device(self):
|
|
2202
|
+
self.add_permissions("dcim.add_interface")
|
|
2203
|
+
|
|
2204
|
+
interface_status = Status.objects.get_for_model(Interface).first()
|
|
2205
|
+
location = self.devices[0].location
|
|
2206
|
+
location_ids = [ancestor.id for ancestor in location.ancestors()]
|
|
2207
|
+
non_valid_locations = Location.objects.exclude(pk__in=location_ids)
|
|
2208
|
+
faulty_vlan = self.vlans[0]
|
|
2209
|
+
faulty_vlan.locations.set([non_valid_locations.first().pk])
|
|
2210
|
+
faulty_vlan.validated_save()
|
|
2211
|
+
faulty_data = {
|
|
2212
|
+
"device": self.devices[0].pk,
|
|
2213
|
+
"name": "Test Vlans Interface",
|
|
2214
|
+
"type": "virtual",
|
|
2215
|
+
"status": interface_status.pk,
|
|
2216
|
+
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
2217
|
+
"parent_interface": self.interfaces[1].pk,
|
|
2218
|
+
"tagged_vlans": [faulty_vlan.pk, self.vlans[1].pk],
|
|
2219
|
+
"untagged_vlan": self.vlans[2].pk,
|
|
2220
|
+
}
|
|
2221
|
+
response = self.client.post(self._get_list_url(), data=faulty_data, format="json", **self.header)
|
|
2222
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2223
|
+
self.assertIn(
|
|
2224
|
+
b"must have the same location as the interface's parent device, or is in one of the parents of the interface's parent device's location, or "
|
|
2225
|
+
b"it must be global.",
|
|
2226
|
+
response.content,
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2201
2229
|
def test_interface_belonging_to_common_device_or_vc_allowed(self):
|
|
2202
2230
|
"""Test parent, bridge, and LAG interfaces belonging to common device or VC is valid"""
|
|
2203
2231
|
self.add_permissions("dcim.add_interface")
|
|
@@ -341,9 +341,25 @@ class InterfaceTestCase(TestCase):
|
|
|
341
341
|
"tagged_vlans": [cls.vlan.pk],
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
def test_interface_form_clean_vlan_location_success(self):
|
|
345
|
+
"""Assert that form validation succeeds when matching locations/parent locations are associated to tagged VLAN"""
|
|
346
|
+
location = self.device.location
|
|
347
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
348
|
+
self.vlan.locations.set([location.id])
|
|
349
|
+
self.data["tagged_vlans"] = [self.vlan]
|
|
350
|
+
form = InterfaceForm(data=self.data, instance=self.interface)
|
|
351
|
+
self.assertTrue(form.is_valid())
|
|
352
|
+
self.vlan.locations.set(location_ids[:2])
|
|
353
|
+
self.data["tagged_vlans"] = [self.vlan]
|
|
354
|
+
form = InterfaceForm(data=self.data, instance=self.interface)
|
|
355
|
+
self.assertTrue(form.is_valid())
|
|
356
|
+
|
|
344
357
|
def test_interface_form_clean_vlan_location_fail(self):
|
|
345
358
|
"""Assert that form validation fails when no matching locations are associated to tagged VLAN"""
|
|
346
|
-
|
|
359
|
+
location = self.device.location
|
|
360
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
361
|
+
self.vlan.locations.set(list(Location.objects.exclude(pk__in=location_ids))[:2])
|
|
362
|
+
self.data["tagged_vlans"] = [self.vlan]
|
|
347
363
|
form = InterfaceForm(data=self.data, instance=self.interface)
|
|
348
364
|
self.assertFalse(form.is_valid())
|
|
349
365
|
|
|
@@ -2376,19 +2376,57 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2376
2376
|
)
|
|
2377
2377
|
|
|
2378
2378
|
def test_error_raised_when_adding_tagged_vlan_with_different_location_from_interface_parent_location(self):
|
|
2379
|
+
intf_status = Status.objects.get_for_model(Interface).first()
|
|
2380
|
+
intf_role = Role.objects.get_for_model(Interface).first()
|
|
2381
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
2382
|
+
child_location = Location.objects.filter(parent__isnull=False, location_type=location_type).first()
|
|
2383
|
+
self.device.location = child_location
|
|
2384
|
+
self.device.validated_save()
|
|
2385
|
+
# Same location as the device
|
|
2386
|
+
interface = Interface.objects.create(
|
|
2387
|
+
name="Test Interface 2",
|
|
2388
|
+
mode=InterfaceModeChoices.MODE_TAGGED,
|
|
2389
|
+
device=self.device,
|
|
2390
|
+
status=intf_status,
|
|
2391
|
+
role=intf_role,
|
|
2392
|
+
)
|
|
2393
|
+
self.other_location_vlan.locations.set([self.device.location.pk])
|
|
2394
|
+
interface.tagged_vlans.set([self.other_location_vlan.pk])
|
|
2395
|
+
|
|
2396
|
+
# One of the parent locations of the device's location
|
|
2397
|
+
interface = Interface.objects.create(
|
|
2398
|
+
name="Test Interface 3",
|
|
2399
|
+
mode=InterfaceModeChoices.MODE_TAGGED,
|
|
2400
|
+
device=self.device,
|
|
2401
|
+
status=intf_status,
|
|
2402
|
+
role=intf_role,
|
|
2403
|
+
)
|
|
2404
|
+
self.other_location_vlan.locations.set([self.device.location.ancestors().first().pk])
|
|
2405
|
+
interface.tagged_vlans.set([self.other_location_vlan.pk])
|
|
2406
|
+
|
|
2379
2407
|
with self.assertRaises(ValidationError) as err:
|
|
2380
2408
|
interface = Interface.objects.create(
|
|
2381
|
-
name="Test Interface",
|
|
2409
|
+
name="Test Interface 1",
|
|
2382
2410
|
mode=InterfaceModeChoices.MODE_TAGGED,
|
|
2383
2411
|
device=self.device,
|
|
2384
|
-
status=
|
|
2385
|
-
role=
|
|
2412
|
+
status=intf_status,
|
|
2413
|
+
role=intf_role,
|
|
2414
|
+
)
|
|
2415
|
+
location_3 = Location.objects.create(
|
|
2416
|
+
name="Invalid VLAN Location",
|
|
2417
|
+
location_type=LocationType.objects.get(name="Campus"),
|
|
2418
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
2386
2419
|
)
|
|
2420
|
+
# clear the valid locations
|
|
2421
|
+
self.other_location_vlan.locations.set([])
|
|
2422
|
+
# assign the invalid location
|
|
2423
|
+
self.other_location_vlan.location = location_3
|
|
2424
|
+
self.other_location_vlan.validated_save()
|
|
2387
2425
|
interface.tagged_vlans.add(self.other_location_vlan)
|
|
2388
2426
|
self.assertEqual(
|
|
2389
2427
|
err.exception.message_dict["tagged_vlans"][0],
|
|
2390
2428
|
f"Tagged VLAN with names {[self.other_location_vlan.name]} must all belong to the "
|
|
2391
|
-
f"same location as the interface's parent device, or it must be global.",
|
|
2429
|
+
f"same location as the interface's parent device, one of the parent locations of the interface's parent device's location, or it must be global.",
|
|
2392
2430
|
)
|
|
2393
2431
|
|
|
2394
2432
|
def test_add_ip_addresses(self):
|
nautobot/dcim/utils.py
CHANGED
|
@@ -139,12 +139,14 @@ def validate_interface_tagged_vlans(instance, model, pk_set):
|
|
|
139
139
|
)
|
|
140
140
|
|
|
141
141
|
# Filter the model objects based on the primary keys passed in kwargs and exclude the ones that have
|
|
142
|
-
# a location that is not the parent's location or None
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
# a location that is not the parent's location, or parent's location's ancestors, or None
|
|
143
|
+
location = getattr(instance.parent, "location", None)
|
|
144
|
+
if location:
|
|
145
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
146
|
+
else:
|
|
147
|
+
location_ids = []
|
|
146
148
|
tagged_vlans = (
|
|
147
|
-
model.objects.filter(pk__in=pk_set).exclude(locations__isnull=True).exclude(locations__in=
|
|
149
|
+
model.objects.filter(pk__in=pk_set).exclude(locations__isnull=True).exclude(locations__in=location_ids)
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
if tagged_vlans.count():
|
|
@@ -152,7 +154,8 @@ def validate_interface_tagged_vlans(instance, model, pk_set):
|
|
|
152
154
|
{
|
|
153
155
|
"tagged_vlans": (
|
|
154
156
|
f"Tagged VLAN with names {list(tagged_vlans.values_list('name', flat=True))} must all belong to the "
|
|
155
|
-
|
|
157
|
+
"same location as the interface's parent device, "
|
|
158
|
+
"one of the parent locations of the interface's parent device's location, or it must be global."
|
|
156
159
|
)
|
|
157
160
|
}
|
|
158
161
|
)
|
nautobot/extras/plugins/views.py
CHANGED
|
@@ -30,6 +30,21 @@ class InstalledAppsView(GenericView):
|
|
|
30
30
|
data = []
|
|
31
31
|
for app in apps.get_app_configs():
|
|
32
32
|
if app.name in settings.PLUGINS:
|
|
33
|
+
try:
|
|
34
|
+
reverse(app.home_view_name)
|
|
35
|
+
home_url = app.home_view_name
|
|
36
|
+
except NoReverseMatch:
|
|
37
|
+
home_url = None
|
|
38
|
+
try:
|
|
39
|
+
reverse(app.config_view_name)
|
|
40
|
+
config_url = app.config_view_name
|
|
41
|
+
except NoReverseMatch:
|
|
42
|
+
config_url = None
|
|
43
|
+
try:
|
|
44
|
+
reverse(app.docs_view_name)
|
|
45
|
+
docs_url = app.docs_view_name
|
|
46
|
+
except NoReverseMatch:
|
|
47
|
+
docs_url = None
|
|
33
48
|
data.append(
|
|
34
49
|
{
|
|
35
50
|
"name": app.verbose_name,
|
|
@@ -40,9 +55,9 @@ class InstalledAppsView(GenericView):
|
|
|
40
55
|
"description": app.description,
|
|
41
56
|
"version": app.version,
|
|
42
57
|
"actions": {
|
|
43
|
-
"home":
|
|
44
|
-
"configure":
|
|
45
|
-
"docs":
|
|
58
|
+
"home": home_url,
|
|
59
|
+
"configure": config_url,
|
|
60
|
+
"docs": docs_url,
|
|
46
61
|
},
|
|
47
62
|
}
|
|
48
63
|
)
|
nautobot/ipam/filters.py
CHANGED
|
@@ -586,13 +586,13 @@ class VLANFilterSet(
|
|
|
586
586
|
fields = ["id", "name", "tags", "vid"]
|
|
587
587
|
|
|
588
588
|
def get_for_device(self, queryset, name, value):
|
|
589
|
-
# TODO: after Location model replaced Site, which was not a hierarchical model, should we consider to include
|
|
590
|
-
# VLANs that belong to the parent/child locations of the `device.location`?
|
|
591
589
|
"""Return all VLANs available to the specified Device(value)."""
|
|
592
590
|
devices = Device.objects.select_related("location").filter(**{f"{name}__in": value})
|
|
593
591
|
if not devices.exists():
|
|
594
592
|
return queryset.none()
|
|
595
593
|
location_ids = list(devices.values_list("location__id", flat=True))
|
|
594
|
+
for location in Location.objects.filter(pk__in=location_ids):
|
|
595
|
+
location_ids.extend([ancestor.id for ancestor in location.ancestors()])
|
|
596
596
|
return queryset.filter(Q(locations__isnull=True) | Q(locations__in=location_ids))
|
|
597
597
|
|
|
598
598
|
|
nautobot/ipam/models.py
CHANGED
|
@@ -890,12 +890,39 @@ class Prefix(PrimaryModel):
|
|
|
890
890
|
)
|
|
891
891
|
return available_ips
|
|
892
892
|
|
|
893
|
+
def get_child_ips(self):
|
|
894
|
+
"""
|
|
895
|
+
Return IP addresses with this prefix as their *direct* parent.
|
|
896
|
+
|
|
897
|
+
Does *not* include IPs that descend from a descendant prefix; if those are desired, use get_all_ips() instead.
|
|
898
|
+
|
|
899
|
+
```
|
|
900
|
+
Prefix 10.0.0.0/16
|
|
901
|
+
IPAddress 10.0.0.1/24
|
|
902
|
+
Prefix 10.0.1.0/24
|
|
903
|
+
IPAddress 10.0.1.1/24
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
In the above example, `<Prefix 10.0.0.0/16>.get_child_ips()` will *only* return 10.0.0.1/24,
|
|
907
|
+
while `<Prefix 10.0.0.0/16>.get_all_ips()` will return *both* 10.0.0.1.24 and 10.0.1.1/24.
|
|
908
|
+
"""
|
|
909
|
+
return self.ip_addresses.all()
|
|
910
|
+
|
|
893
911
|
def get_all_ips(self):
|
|
894
912
|
"""
|
|
895
913
|
Return all IP addresses contained within this prefix, including child prefixes' IP addresses.
|
|
896
914
|
|
|
897
|
-
|
|
898
|
-
|
|
915
|
+
This is distinct from the behavior of `get_child_ips()` and in *most* cases is probably preferred.
|
|
916
|
+
|
|
917
|
+
```
|
|
918
|
+
Prefix 10.0.0.0/16
|
|
919
|
+
IPAddress 10.0.0.1/24
|
|
920
|
+
Prefix 10.0.1.0/24
|
|
921
|
+
IPAddress 10.0.1.1/24
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
In the above example, `<Prefix 10.0.0.0/16>.get_child_ips()` will *only* return 10.0.0.1/24,
|
|
925
|
+
while `<Prefix 10.0.0.0/16>.get_all_ips()` will return *both* 10.0.0.1.24 and 10.0.1.1/24.
|
|
899
926
|
"""
|
|
900
927
|
return IPAddress.objects.filter(
|
|
901
928
|
parent__namespace=self.namespace, host__gte=self.network, host__lte=self.broadcast
|
|
@@ -150,9 +150,9 @@
|
|
|
150
150
|
</tr>
|
|
151
151
|
</table>
|
|
152
152
|
</div>
|
|
153
|
-
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
|
154
153
|
{% endblock content_right_page %}
|
|
155
154
|
|
|
156
155
|
{% block content_full_width_page %}
|
|
157
|
-
|
|
156
|
+
{% include 'utilities/obj_table.html' with table=parent_prefixes_table table_template='panel_table.html' heading='Parent Prefixes' %}
|
|
157
|
+
{% include 'utilities/obj_table.html' with table=related_ips_table table_template='panel_table.html' heading='Related IP Addresses' %}
|
|
158
158
|
{% endblock content_full_width_page %}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
{% render_table interface_table 'inc/table.html' %}
|
|
22
22
|
</div>
|
|
23
23
|
</form>
|
|
24
|
+
{% if interface_table.paginator.num_pages > 1 %}
|
|
25
|
+
{% include "inc/paginator.html" with paginator=interface_table.paginator page=interface_table.page %}
|
|
26
|
+
{% endif %}
|
|
24
27
|
{% table_config_form interface_table %}
|
|
25
28
|
{% endblock content %}
|
|
26
29
|
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
{% render_table vm_interface_table 'inc/table.html' %}
|
|
22
22
|
</div>
|
|
23
23
|
</form>
|
|
24
|
+
{% if vm_interface_table.paginator.num_pages > 1 %}
|
|
25
|
+
{% include "inc/paginator.html" with paginator=vm_interface_table.paginator page=vm_interface_table.page %}
|
|
26
|
+
{% endif %}
|
|
24
27
|
{% table_config_form vm_interface_table %}
|
|
25
28
|
{% endblock content %}
|
|
26
29
|
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
{% endblock content_left_page %}
|
|
118
118
|
|
|
119
119
|
{% block content_right_page %}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
{% include "utilities/obj_table.html" with table=parent_prefix_table table_template="panel_table.html" heading="Parent Prefixes" %}
|
|
121
|
+
{% include "utilities/obj_table.html" with table=vrf_table table_template="panel_table.html" heading="Assigned VRFs" %}
|
|
122
|
+
{% include "utilities/obj_table.html" with table=cloud_network_table table_template="panel_table.html" heading="Assigned Cloud Networks" %}
|
|
123
123
|
{% endblock content_right_page %}
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
{% endblock content_left_page %}
|
|
25
25
|
|
|
26
26
|
{% block content_right_page %}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
{% include 'utilities/obj_table.html' with table=importing_vrfs_table table_template='panel_table.html' heading="Importing VRFs" %}
|
|
28
|
+
{% include 'utilities/obj_table.html' with table=exporting_vrfs_table table_template='panel_table.html' heading="Exporting VRFs" %}
|
|
29
29
|
{% endblock content_right_page %}
|
|
@@ -38,8 +38,11 @@
|
|
|
38
38
|
{% endblock content_left_page %}
|
|
39
39
|
|
|
40
40
|
{% block content_right_page %}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{% include 'panel_table.html' with table=prefix_table heading="Assigned Prefixes" %}
|
|
44
|
-
{% include 'panel_table.html' with table=device_table heading="Assigned Devices" %}
|
|
41
|
+
{% include 'utilities/obj_table.html' with table=import_targets_table table_template='panel_table.html' heading="Import Route Targets" %}
|
|
42
|
+
{% include 'utilities/obj_table.html' with table=export_targets_table table_template='panel_table.html' heading="Export Route Targets" %}
|
|
45
43
|
{% endblock content_right_page %}
|
|
44
|
+
|
|
45
|
+
{% block content_full_width_page %}
|
|
46
|
+
{% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading="Assigned Prefixes" %}
|
|
47
|
+
{% include 'utilities/obj_table.html' with table=device_table table_template='panel_table.html' heading="Assigned Devices" %}
|
|
48
|
+
{% endblock content_full_width_page %}
|
|
@@ -555,8 +555,9 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
555
555
|
IPAddress.objects.create(address="10.0.2.1/24", status=self.status, namespace=self.namespace),
|
|
556
556
|
IPAddress.objects.create(address="10.0.3.1/24", status=self.status, namespace=self.namespace),
|
|
557
557
|
)
|
|
558
|
+
self.assertQuerysetEqualAndNotEmpty(parent_prefix.ip_addresses.all(), parent_prefix.get_child_ips())
|
|
559
|
+
self.assertQuerysetEqualAndNotEmpty(parent_prefix.ip_addresses.all(), parent_prefix.get_all_ips())
|
|
558
560
|
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
|
559
|
-
|
|
560
561
|
# Global container should return all children
|
|
561
562
|
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
|
|
562
563
|
|
|
@@ -566,6 +567,8 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
566
567
|
IPAddress.objects.create(address="20.0.4.0/31", status=self.status, namespace=self.namespace),
|
|
567
568
|
IPAddress.objects.create(address="20.0.4.1/31", status=self.status, namespace=self.namespace),
|
|
568
569
|
)
|
|
570
|
+
self.assertQuerysetEqualAndNotEmpty(parent_prefix_31.ip_addresses.all(), parent_prefix_31.get_child_ips())
|
|
571
|
+
self.assertQuerysetEqualAndNotEmpty(parent_prefix_31.ip_addresses.all(), parent_prefix_31.get_all_ips())
|
|
569
572
|
child_ip_pks = {p.pk for p in parent_prefix_31.ip_addresses.all()}
|
|
570
573
|
self.assertSetEqual(child_ip_pks, {ips_31[0].pk, ips_31[1].pk})
|
|
571
574
|
|
|
@@ -660,10 +663,22 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
660
663
|
slash25 = Prefix.objects.create(prefix="10.0.0.128/25", status=self.status, namespace=self.namespace)
|
|
661
664
|
self.assertEqual(prefix.get_utilization(), (192, 256))
|
|
662
665
|
|
|
663
|
-
# Create 32 IPAddresses within the Prefix
|
|
666
|
+
# Create 32 IPAddresses within the /26 Prefix
|
|
664
667
|
for i in range(1, 33):
|
|
665
668
|
IPAddress.objects.create(address=f"10.0.0.{i}/32", status=self.status, namespace=self.namespace)
|
|
666
669
|
|
|
670
|
+
# Assert differing behavior of get_all_ips() versus get_child_ips() for the /24 and /26 prefixes
|
|
671
|
+
self.assertQuerysetEqual(prefix.get_child_ips(), IPAddress.objects.none())
|
|
672
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
673
|
+
prefix.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
674
|
+
)
|
|
675
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
676
|
+
slash26.get_child_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
677
|
+
)
|
|
678
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
679
|
+
slash26.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
680
|
+
)
|
|
681
|
+
|
|
667
682
|
# The parent prefix utilization does not change because the ip addresses are parented to the child /26 prefix.
|
|
668
683
|
self.assertEqual(prefix.get_utilization(), (192, 256))
|
|
669
684
|
|
|
@@ -674,6 +689,17 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
674
689
|
IPAddress.objects.create(address="10.0.0.0/32", status=self.status, namespace=self.namespace)
|
|
675
690
|
IPAddress.objects.create(address="10.0.0.63/32", status=self.status, namespace=self.namespace)
|
|
676
691
|
|
|
692
|
+
self.assertQuerysetEqual(prefix.get_child_ips(), IPAddress.objects.none())
|
|
693
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
694
|
+
prefix.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
695
|
+
)
|
|
696
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
697
|
+
slash26.get_child_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
698
|
+
)
|
|
699
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
700
|
+
slash26.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
701
|
+
)
|
|
702
|
+
|
|
677
703
|
# The /26 denominator will change to 64
|
|
678
704
|
self.assertEqual(slash26.get_utilization(), (34, 64))
|
|
679
705
|
|
|
@@ -688,30 +714,60 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
688
714
|
pool.save()
|
|
689
715
|
self.assertEqual(slash25.get_utilization(), (4, 126))
|
|
690
716
|
|
|
717
|
+
# Further distinguishing between get_child_ips() and get_all_ips():
|
|
718
|
+
IPAddress.objects.create(address="10.0.0.64/32", status=self.status, namespace=self.namespace)
|
|
719
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
720
|
+
prefix.get_child_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.64/26")
|
|
721
|
+
)
|
|
722
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
723
|
+
prefix.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
slash27 = Prefix.objects.create(prefix="10.0.0.0/27", status=self.status, namespace=self.namespace)
|
|
727
|
+
self.assertEqual(slash27.get_utilization(), (32, 32))
|
|
728
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
729
|
+
prefix.get_child_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.64/26")
|
|
730
|
+
)
|
|
731
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
732
|
+
prefix.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/24")
|
|
733
|
+
)
|
|
734
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
735
|
+
slash26.get_child_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.32/27")
|
|
736
|
+
)
|
|
737
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
738
|
+
slash26.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/26")
|
|
739
|
+
)
|
|
740
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
741
|
+
slash27.get_child_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/27")
|
|
742
|
+
)
|
|
743
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
744
|
+
slash27.get_all_ips(), IPAddress.objects.filter(host__net_host_contained="10.0.0.0/27")
|
|
745
|
+
)
|
|
746
|
+
|
|
691
747
|
# IPv4 Non-container Prefix /31, network and broadcast addresses count toward utilization
|
|
692
|
-
|
|
748
|
+
slash31 = Prefix.objects.create(prefix="10.0.1.0/31", status=self.status, namespace=self.namespace)
|
|
693
749
|
IPAddress.objects.create(address="10.0.1.0/32", status=self.status, namespace=self.namespace)
|
|
694
750
|
IPAddress.objects.create(address="10.0.1.1/32", status=self.status, namespace=self.namespace)
|
|
695
|
-
self.assertEqual(
|
|
751
|
+
self.assertEqual(slash31.get_utilization(), (2, 2))
|
|
696
752
|
|
|
697
753
|
# IPv6 Non-container Prefix, first and last addresses count toward utilization
|
|
698
|
-
|
|
754
|
+
slash124_1 = Prefix.objects.create(prefix="aaab::/124", status=self.status, namespace=self.namespace)
|
|
699
755
|
IPAddress.objects.create(address="aaab::1/128", status=self.status, namespace=self.namespace)
|
|
700
756
|
IPAddress.objects.create(address="aaab::2/128", status=self.status, namespace=self.namespace)
|
|
701
|
-
self.assertEqual(
|
|
757
|
+
self.assertEqual(slash124_1.get_utilization(), (2, 16))
|
|
702
758
|
|
|
703
|
-
|
|
759
|
+
slash124_2 = Prefix.objects.create(prefix="aaaa::/124", status=self.status, namespace=self.namespace)
|
|
704
760
|
IPAddress.objects.create(address="aaaa::0/128", status=self.status, namespace=self.namespace)
|
|
705
761
|
IPAddress.objects.create(address="aaaa::f/128", status=self.status, namespace=self.namespace)
|
|
706
|
-
self.assertEqual(
|
|
762
|
+
self.assertEqual(slash124_2.get_utilization(), (2, 16))
|
|
707
763
|
|
|
708
764
|
# single address prefixes
|
|
709
|
-
|
|
765
|
+
slash128 = Prefix.objects.create(prefix="cccc::1/128", status=self.status, namespace=self.namespace)
|
|
710
766
|
IPAddress.objects.create(address="cccc::1/128", status=self.status, namespace=self.namespace)
|
|
711
|
-
self.assertEqual(
|
|
712
|
-
|
|
767
|
+
self.assertEqual(slash128.get_utilization(), (1, 1))
|
|
768
|
+
slash32 = Prefix.objects.create(prefix="1.1.1.1/32", status=self.status, namespace=self.namespace)
|
|
713
769
|
IPAddress.objects.create(address="1.1.1.1/32", status=self.status, namespace=self.namespace)
|
|
714
|
-
self.assertEqual(
|
|
770
|
+
self.assertEqual(slash32.get_utilization(), (1, 1))
|
|
715
771
|
|
|
716
772
|
# Large Prefix
|
|
717
773
|
large_prefix = Prefix.objects.create(
|