nautobot 2.4.21__py3-none-any.whl → 2.4.22__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/apps/choices.py +4 -0
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/views.py +6 -2
- nautobot/core/cli/migrate_deprecated_templates.py +28 -9
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/settings.py +6 -0
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templatetags/helpers.py +9 -7
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/test_templatetags_helpers.py +6 -0
- nautobot/core/tests/test_ui.py +49 -1
- nautobot/core/tests/test_utils.py +41 -1
- nautobot/core/ui/object_detail.py +7 -2
- nautobot/core/urls.py +7 -8
- nautobot/core/utils/filtering.py +11 -1
- nautobot/core/utils/lookup.py +46 -0
- nautobot/core/views/mixins.py +21 -16
- nautobot/dcim/api/serializers.py +3 -0
- nautobot/dcim/choices.py +49 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/filters/__init__.py +7 -0
- nautobot/dcim/forms.py +89 -3
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +21 -0
- nautobot/dcim/tables/devices.py +14 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/templates/dcim/interface.html +8 -0
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/tests/test_api.py +186 -6
- nautobot/dcim/tests/test_filters.py +32 -0
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +265 -0
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +64 -1
- nautobot/dcim/views.py +86 -77
- nautobot/extras/forms/forms.py +3 -1
- nautobot/extras/templates/extras/plugin_detail.html +2 -2
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +1 -1
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/views.py +2 -2
- nautobot/project-static/js/forms.js +92 -14
- nautobot/virtualization/tests/test_models.py +4 -2
- nautobot/virtualization/views.py +1 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/METADATA +4 -4
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/RECORD +54 -51
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/WHEEL +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/entry_points.txt +0 -0
|
@@ -3,7 +3,7 @@ from django.test import override_settings
|
|
|
3
3
|
|
|
4
4
|
from nautobot.core.graphql import execute_query
|
|
5
5
|
from nautobot.core.testing import create_test_user, TestCase
|
|
6
|
-
from nautobot.dcim.choices import InterfaceTypeChoices
|
|
6
|
+
from nautobot.dcim.choices import InterfaceDuplexChoices, InterfaceSpeedChoices, InterfaceTypeChoices
|
|
7
7
|
from nautobot.dcim.models import (
|
|
8
8
|
Controller,
|
|
9
9
|
Device,
|
|
@@ -52,6 +52,22 @@ class GraphQLTestCase(TestCase):
|
|
|
52
52
|
type=InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
53
53
|
mac_address=None,
|
|
54
54
|
),
|
|
55
|
+
Interface.objects.create(
|
|
56
|
+
device=self.device,
|
|
57
|
+
name="eth2",
|
|
58
|
+
status=interface_status,
|
|
59
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
60
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
61
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
62
|
+
),
|
|
63
|
+
Interface.objects.create(
|
|
64
|
+
device=self.device,
|
|
65
|
+
name="eth3",
|
|
66
|
+
status=interface_status,
|
|
67
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
68
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
69
|
+
duplex="",
|
|
70
|
+
),
|
|
55
71
|
)
|
|
56
72
|
for interface in self.interfaces:
|
|
57
73
|
interface.validated_save()
|
|
@@ -135,3 +151,30 @@ class GraphQLTestCase(TestCase):
|
|
|
135
151
|
self.assertFalse(resp["data"].get("error"))
|
|
136
152
|
for device in resp["data"]["devices"]:
|
|
137
153
|
self.assertNotEqual(device["serial"], "")
|
|
154
|
+
|
|
155
|
+
with self.subTest("interface speed/duplex fields on device query"):
|
|
156
|
+
query = "query { devices { name interfaces { name speed duplex } } }"
|
|
157
|
+
resp = execute_query(query, user=self.user).to_dict()
|
|
158
|
+
self.assertFalse(resp["data"].get("error"))
|
|
159
|
+
interfaces = [i for d in resp["data"]["devices"] if d["name"] == self.device.name for i in d["interfaces"]]
|
|
160
|
+
eth2 = next(i for i in interfaces if i["name"] == "eth2")
|
|
161
|
+
eth3 = next(i for i in interfaces if i["name"] == "eth3")
|
|
162
|
+
self.assertEqual(eth2["speed"], InterfaceSpeedChoices.SPEED_1G)
|
|
163
|
+
self.assertEqual(eth2["duplex"].lower(), InterfaceDuplexChoices.DUPLEX_FULL)
|
|
164
|
+
self.assertEqual(eth3["speed"], InterfaceSpeedChoices.SPEED_10G)
|
|
165
|
+
self.assertEqual(eth3["duplex"], None)
|
|
166
|
+
|
|
167
|
+
with self.subTest("interfaces root filter by speed and duplex"):
|
|
168
|
+
query = f"query {{ interfaces(speed: {InterfaceSpeedChoices.SPEED_1G}) {{ name }} }}"
|
|
169
|
+
resp = execute_query(query, user=self.user).to_dict()
|
|
170
|
+
self.assertFalse(resp["data"].get("error"))
|
|
171
|
+
names = {i["name"] for i in resp["data"]["interfaces"]}
|
|
172
|
+
self.assertIn("eth2", names)
|
|
173
|
+
self.assertNotIn("eth3", names)
|
|
174
|
+
|
|
175
|
+
query = 'query { interfaces(duplex: ["full"]) { name } }'
|
|
176
|
+
resp = execute_query(query, user=self.user).to_dict()
|
|
177
|
+
self.assertFalse(resp["data"].get("error"))
|
|
178
|
+
names = {i["name"] for i in resp["data"]["interfaces"]}
|
|
179
|
+
self.assertIn("eth2", names)
|
|
180
|
+
self.assertNotIn("eth3", names)
|
|
@@ -16,7 +16,9 @@ from nautobot.dcim.choices import (
|
|
|
16
16
|
CableTypeChoices,
|
|
17
17
|
ConsolePortTypeChoices,
|
|
18
18
|
DeviceFaceChoices,
|
|
19
|
+
InterfaceDuplexChoices,
|
|
19
20
|
InterfaceModeChoices,
|
|
21
|
+
InterfaceSpeedChoices,
|
|
20
22
|
InterfaceTypeChoices,
|
|
21
23
|
PortTypeChoices,
|
|
22
24
|
PowerFeedBreakerPoleChoices,
|
|
@@ -724,6 +726,101 @@ class InterfaceTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, Tes
|
|
|
724
726
|
first_status = Status.objects.get_for_model(Interface).first()
|
|
725
727
|
self.assertIsNotNone(device_2.interfaces.get(name="Test_Template_1").status, first_status)
|
|
726
728
|
|
|
729
|
+
def test_speed_disallowed_for_lag_virtual_wireless(self):
|
|
730
|
+
"""speed must be None for LAG, virtual, and wireless templates."""
|
|
731
|
+
manufacturer = Manufacturer.objects.first()
|
|
732
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SpeedGuard 1000")
|
|
733
|
+
|
|
734
|
+
for if_type in (
|
|
735
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
736
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
737
|
+
InterfaceTypeChoices.TYPE_80211N,
|
|
738
|
+
):
|
|
739
|
+
with self.subTest(if_type=if_type):
|
|
740
|
+
with self.assertRaises(ValidationError) as cm:
|
|
741
|
+
InterfaceTemplate(
|
|
742
|
+
device_type=device_type,
|
|
743
|
+
name=f"bad-{if_type}",
|
|
744
|
+
type=if_type,
|
|
745
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
746
|
+
).full_clean()
|
|
747
|
+
self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
|
|
748
|
+
|
|
749
|
+
def test_duplex_disallowed_for_lag_virtual_wireless(self):
|
|
750
|
+
"""duplex must be blank for LAG, virtual, and wireless templates."""
|
|
751
|
+
manufacturer = Manufacturer.objects.first()
|
|
752
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="DuplexGuard 1000")
|
|
753
|
+
|
|
754
|
+
for itype in (
|
|
755
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
756
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
757
|
+
InterfaceTypeChoices.TYPE_80211N,
|
|
758
|
+
):
|
|
759
|
+
with self.assertRaises(ValidationError):
|
|
760
|
+
InterfaceTemplate(
|
|
761
|
+
device_type=device_type,
|
|
762
|
+
name=f"bad-{itype}",
|
|
763
|
+
type=itype,
|
|
764
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
765
|
+
).full_clean()
|
|
766
|
+
|
|
767
|
+
def test_duplex_disallowed_for_non_base_t(self):
|
|
768
|
+
"""duplex must be blank for non-BASE-T physical types (e.g., SFP)."""
|
|
769
|
+
manufacturer = Manufacturer.objects.first()
|
|
770
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SfpGuard 1000")
|
|
771
|
+
|
|
772
|
+
with self.assertRaises(ValidationError) as cm:
|
|
773
|
+
InterfaceTemplate(
|
|
774
|
+
device_type=device_type,
|
|
775
|
+
name="sfp0",
|
|
776
|
+
type=InterfaceTypeChoices.TYPE_1GE_SFP,
|
|
777
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
778
|
+
).full_clean()
|
|
779
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
780
|
+
|
|
781
|
+
def test_duplex_and_speed_allowed_for_base_t(self):
|
|
782
|
+
"""BASE-T physical types accept duplex and speed values."""
|
|
783
|
+
manufacturer = Manufacturer.objects.first()
|
|
784
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="CopperOK 1000")
|
|
785
|
+
|
|
786
|
+
tmpl = InterfaceTemplate(
|
|
787
|
+
device_type=device_type,
|
|
788
|
+
name="eth0",
|
|
789
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
790
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
791
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
792
|
+
)
|
|
793
|
+
tmpl.full_clean() # should not raise
|
|
794
|
+
|
|
795
|
+
def test_instantiation_propagates_speed_and_duplex(self):
|
|
796
|
+
"""Interface created from template inherits speed and duplex."""
|
|
797
|
+
statuses = Status.objects.get_for_model(Device)
|
|
798
|
+
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
799
|
+
manufacturer = Manufacturer.objects.first()
|
|
800
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
801
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="Propagate 2000")
|
|
802
|
+
|
|
803
|
+
InterfaceTemplate.objects.create(
|
|
804
|
+
device_type=device_type,
|
|
805
|
+
name="EthX",
|
|
806
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
807
|
+
mgmt_only=False,
|
|
808
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
809
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
device = Device.objects.create(
|
|
813
|
+
device_type=device_type,
|
|
814
|
+
role=device_role,
|
|
815
|
+
status=statuses[0],
|
|
816
|
+
name="Device-Prop",
|
|
817
|
+
location=location,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
iface = device.interfaces.get(name="EthX")
|
|
821
|
+
self.assertEqual(iface.speed, InterfaceSpeedChoices.SPEED_1G)
|
|
822
|
+
self.assertEqual(iface.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
|
|
823
|
+
|
|
727
824
|
|
|
728
825
|
class InterfaceRedundancyGroupTestCase(ModelTestCases.BaseModelTestCase):
|
|
729
826
|
model = InterfaceRedundancyGroup
|
|
@@ -2695,6 +2792,7 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2695
2792
|
name="VLAN 1", vid=100, location=location, status=vlan_status, vlan_group=vlan_group
|
|
2696
2793
|
)
|
|
2697
2794
|
status = Status.objects.get_for_model(Device).first()
|
|
2795
|
+
cls.intf_status = Status.objects.get_for_model(Interface).first()
|
|
2698
2796
|
cls.device = Device.objects.create(
|
|
2699
2797
|
name="Device 1",
|
|
2700
2798
|
device_type=devicetype,
|
|
@@ -2924,6 +3022,173 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2924
3022
|
self.device.refresh_from_db()
|
|
2925
3023
|
self.assertEqual(self.device.primary_ip6, None)
|
|
2926
3024
|
|
|
3025
|
+
def _assert_invalid_speed_duplex(self, if_type, speed=None, duplex="", expected_error=""):
|
|
3026
|
+
iface = Interface(
|
|
3027
|
+
device=self.device,
|
|
3028
|
+
name=f"test-{if_type}",
|
|
3029
|
+
type=if_type,
|
|
3030
|
+
status=self.intf_status,
|
|
3031
|
+
speed=speed,
|
|
3032
|
+
duplex=duplex,
|
|
3033
|
+
)
|
|
3034
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3035
|
+
iface.full_clean()
|
|
3036
|
+
self.assertIn(expected_error, str(cm.exception))
|
|
3037
|
+
|
|
3038
|
+
def test_disallowed_speed_and_duplex_matrix(self):
|
|
3039
|
+
"""Test that interface types with no speed or duplex disallow those settings."""
|
|
3040
|
+
test_cases = [
|
|
3041
|
+
# LAG
|
|
3042
|
+
(
|
|
3043
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
3044
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3045
|
+
None,
|
|
3046
|
+
"Speed is not applicable to this interface type.",
|
|
3047
|
+
),
|
|
3048
|
+
(
|
|
3049
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
3050
|
+
None,
|
|
3051
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3052
|
+
"Duplex is not applicable to this interface type.",
|
|
3053
|
+
),
|
|
3054
|
+
# Virtual
|
|
3055
|
+
(
|
|
3056
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
3057
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3058
|
+
None,
|
|
3059
|
+
"Speed is not applicable to this interface type.",
|
|
3060
|
+
),
|
|
3061
|
+
(
|
|
3062
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
3063
|
+
None,
|
|
3064
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3065
|
+
"Duplex is not applicable to this interface type.",
|
|
3066
|
+
),
|
|
3067
|
+
# Wireless
|
|
3068
|
+
(
|
|
3069
|
+
InterfaceTypeChoices.TYPE_80211AC,
|
|
3070
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3071
|
+
None,
|
|
3072
|
+
"Speed is not applicable to this interface type.",
|
|
3073
|
+
),
|
|
3074
|
+
(
|
|
3075
|
+
InterfaceTypeChoices.TYPE_80211AC,
|
|
3076
|
+
None,
|
|
3077
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3078
|
+
"Duplex is not applicable to this interface type.",
|
|
3079
|
+
),
|
|
3080
|
+
# Copper (negative speed is invalid)
|
|
3081
|
+
(InterfaceTypeChoices.TYPE_1GE_FIXED, -100, None, "Ensure this value is greater than or equal to 0."),
|
|
3082
|
+
# Copper (speed as a string is invalid)
|
|
3083
|
+
(InterfaceTypeChoices.TYPE_1GE_FIXED, "100 Mbps", None, "value must be an integer."),
|
|
3084
|
+
# Copper (invalid duplex is invalid)
|
|
3085
|
+
(
|
|
3086
|
+
InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
3087
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3088
|
+
"invalid",
|
|
3089
|
+
"Value 'invalid' is not a valid choice.",
|
|
3090
|
+
),
|
|
3091
|
+
# Optical (no duplex allowed)
|
|
3092
|
+
(
|
|
3093
|
+
InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
3094
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3095
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3096
|
+
"Duplex is only applicable to copper twisted-pair interfaces.",
|
|
3097
|
+
),
|
|
3098
|
+
]
|
|
3099
|
+
for if_type, speed, duplex, expected_error in test_cases:
|
|
3100
|
+
with self.subTest(f"{if_type} with speed={speed} and duplex={duplex}"):
|
|
3101
|
+
self._assert_invalid_speed_duplex(if_type, speed, duplex, expected_error)
|
|
3102
|
+
|
|
3103
|
+
def test_copper_allows_duplex_and_non_negative_speed(self):
|
|
3104
|
+
"""Test that copper interfaces allow duplex and non-negative speed."""
|
|
3105
|
+
iface = Interface(
|
|
3106
|
+
device=self.device,
|
|
3107
|
+
name="eth1",
|
|
3108
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED, # 1000BASE-T
|
|
3109
|
+
status=self.intf_status,
|
|
3110
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
3111
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3112
|
+
)
|
|
3113
|
+
# Should not raise
|
|
3114
|
+
iface.full_clean()
|
|
3115
|
+
|
|
3116
|
+
iface.speed = 0
|
|
3117
|
+
iface.full_clean()
|
|
3118
|
+
|
|
3119
|
+
def test_lag_allows_no_speed_or_duplex(self):
|
|
3120
|
+
"""Test that LAG interfaces pass validation when speed and duplex are not set."""
|
|
3121
|
+
iface = Interface(
|
|
3122
|
+
device=self.device,
|
|
3123
|
+
name="Port-Channel1",
|
|
3124
|
+
type=InterfaceTypeChoices.TYPE_LAG,
|
|
3125
|
+
status=self.intf_status,
|
|
3126
|
+
)
|
|
3127
|
+
# Should not raise when speed and duplex are not set
|
|
3128
|
+
iface.full_clean()
|
|
3129
|
+
|
|
3130
|
+
def test_optical_disallows_duplex_allows_speed(self):
|
|
3131
|
+
"""Test that optical interfaces do not allow duplex and allow positive speed."""
|
|
3132
|
+
# Duplex set should error
|
|
3133
|
+
iface_bad = Interface(
|
|
3134
|
+
device=self.device,
|
|
3135
|
+
name="xe0",
|
|
3136
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
3137
|
+
status=self.intf_status,
|
|
3138
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3139
|
+
)
|
|
3140
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3141
|
+
iface_bad.full_clean()
|
|
3142
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
3143
|
+
|
|
3144
|
+
# Speed positive should pass
|
|
3145
|
+
iface_ok = Interface(
|
|
3146
|
+
device=self.device,
|
|
3147
|
+
name="xe1",
|
|
3148
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
3149
|
+
status=self.intf_status,
|
|
3150
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
3151
|
+
)
|
|
3152
|
+
iface_ok.full_clean()
|
|
3153
|
+
|
|
3154
|
+
def test_changing_copper_interface_with_speed_and_duplex_to_optical_fails(self):
|
|
3155
|
+
"""Test that changing a copper interface with speed and duplex to an optical interface fails."""
|
|
3156
|
+
|
|
3157
|
+
with self.subTest("speed"):
|
|
3158
|
+
iface = Interface(
|
|
3159
|
+
device=self.device,
|
|
3160
|
+
name="eth3",
|
|
3161
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
3162
|
+
status=self.intf_status,
|
|
3163
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
3164
|
+
)
|
|
3165
|
+
iface.full_clean()
|
|
3166
|
+
|
|
3167
|
+
iface.type = InterfaceTypeChoices.TYPE_LAG
|
|
3168
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3169
|
+
iface.full_clean()
|
|
3170
|
+
self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
|
|
3171
|
+
|
|
3172
|
+
with self.subTest("duplex"):
|
|
3173
|
+
iface = Interface(
|
|
3174
|
+
device=self.device,
|
|
3175
|
+
name="eth3",
|
|
3176
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
3177
|
+
status=self.intf_status,
|
|
3178
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3179
|
+
)
|
|
3180
|
+
iface.full_clean()
|
|
3181
|
+
|
|
3182
|
+
iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
|
3183
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3184
|
+
iface.full_clean()
|
|
3185
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
3186
|
+
|
|
3187
|
+
iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
|
3188
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3189
|
+
iface.full_clean()
|
|
3190
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
3191
|
+
|
|
2927
3192
|
|
|
2928
3193
|
class SoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
|
|
2929
3194
|
model = SoftwareImageFile
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
|
|
3
|
+
from nautobot.dcim.choices import InterfaceDuplexChoices, InterfaceSpeedChoices, InterfaceTypeChoices
|
|
4
|
+
from nautobot.dcim.models import Device, DeviceType, Interface, InterfaceTemplate, Location, LocationType, Manufacturer
|
|
5
|
+
from nautobot.dcim.tables.devices import DeviceModuleInterfaceTable, InterfaceTable
|
|
6
|
+
from nautobot.dcim.tables.devicetypes import InterfaceTemplateTable
|
|
7
|
+
from nautobot.extras.models import Role, Status
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InterfaceTableRenderMixin:
|
|
11
|
+
"""Mixin for testing render_speed methods on interface tables."""
|
|
12
|
+
|
|
13
|
+
table_class = None
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def setUpTestData(cls):
|
|
17
|
+
manufacturer = Manufacturer.objects.create(name="Test Manufacturer")
|
|
18
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="Test Device Type")
|
|
19
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
20
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
21
|
+
location = Location.objects.filter(location_type=location_type).first()
|
|
22
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
23
|
+
cls.interface_status = Status.objects.get_for_model(Interface).first()
|
|
24
|
+
|
|
25
|
+
cls.device = Device.objects.create(
|
|
26
|
+
name="Test Device",
|
|
27
|
+
device_type=device_type,
|
|
28
|
+
role=device_role,
|
|
29
|
+
location=location,
|
|
30
|
+
status=device_status,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def test_render_speed_duplex_with_value(self):
|
|
34
|
+
"""Test that the table renders humanized speed values."""
|
|
35
|
+
interface = Interface.objects.create(
|
|
36
|
+
device=self.device,
|
|
37
|
+
name="eth0",
|
|
38
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
39
|
+
status=self.interface_status,
|
|
40
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
41
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
queryset = Interface.objects.filter(pk=interface.pk)
|
|
45
|
+
table = self.table_class(queryset) # pylint: disable=not-callable
|
|
46
|
+
bound_row = table.rows[0]
|
|
47
|
+
rendered_speed = bound_row.get_cell("speed")
|
|
48
|
+
rendered_duplex = bound_row.get_cell("duplex")
|
|
49
|
+
|
|
50
|
+
self.assertEqual(rendered_speed, "1 Gbps")
|
|
51
|
+
self.assertEqual(rendered_duplex, "Full")
|
|
52
|
+
|
|
53
|
+
def test_render_speed_duplex_with_none(self):
|
|
54
|
+
"""Test that the table handles None speed value and renders an emdash."""
|
|
55
|
+
emdash = "\u2014"
|
|
56
|
+
interface = Interface.objects.create(
|
|
57
|
+
device=self.device,
|
|
58
|
+
name="eth1",
|
|
59
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
60
|
+
status=self.interface_status,
|
|
61
|
+
speed=None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
queryset = Interface.objects.filter(pk=interface.pk)
|
|
65
|
+
table = self.table_class(queryset) # pylint: disable=not-callable
|
|
66
|
+
bound_row = table.rows[0]
|
|
67
|
+
rendered_speed = bound_row.get_cell("speed")
|
|
68
|
+
rendered_duplex = bound_row.get_cell("duplex")
|
|
69
|
+
|
|
70
|
+
self.assertEqual(rendered_speed, emdash)
|
|
71
|
+
self.assertEqual(rendered_duplex, emdash)
|
|
72
|
+
|
|
73
|
+
def test_render_speed_various(self):
|
|
74
|
+
"""Test that the table correctly humanizes various speed values."""
|
|
75
|
+
# Test all speed choices defined in InterfaceSpeedChoices
|
|
76
|
+
for speed_value, expected_output in InterfaceSpeedChoices.CHOICES:
|
|
77
|
+
with self.subTest(speed_value=speed_value, expected=expected_output):
|
|
78
|
+
interface = Interface.objects.create(
|
|
79
|
+
device=self.device,
|
|
80
|
+
name=f"eth-{speed_value}",
|
|
81
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
82
|
+
status=self.interface_status,
|
|
83
|
+
speed=speed_value,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
queryset = Interface.objects.filter(pk=interface.pk)
|
|
87
|
+
table = self.table_class(queryset) # pylint: disable=not-callable
|
|
88
|
+
bound_row = table.rows[0]
|
|
89
|
+
rendered_speed = bound_row.get_cell("speed")
|
|
90
|
+
|
|
91
|
+
self.assertEqual(rendered_speed, expected_output)
|
|
92
|
+
|
|
93
|
+
def test_render_duplex_various(self):
|
|
94
|
+
"""Test that the table correctly renders various duplex values."""
|
|
95
|
+
for duplex_value, expected_output in InterfaceDuplexChoices.CHOICES:
|
|
96
|
+
with self.subTest(duplex_value=duplex_value, expected=expected_output):
|
|
97
|
+
interface = Interface.objects.create(
|
|
98
|
+
device=self.device,
|
|
99
|
+
name=f"eth-{duplex_value}",
|
|
100
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
101
|
+
status=self.interface_status,
|
|
102
|
+
duplex=duplex_value,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
queryset = Interface.objects.filter(pk=interface.pk)
|
|
106
|
+
table = self.table_class(queryset) # pylint: disable=not-callable
|
|
107
|
+
bound_row = table.rows[0]
|
|
108
|
+
rendered_duplex = bound_row.get_cell("duplex")
|
|
109
|
+
|
|
110
|
+
self.assertEqual(rendered_duplex, expected_output)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class InterfaceTableTestCase(InterfaceTableRenderMixin, TestCase):
|
|
114
|
+
"""Test cases for InterfaceTable."""
|
|
115
|
+
|
|
116
|
+
table_class = InterfaceTable
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class DeviceModuleInterfaceTableTestCase(InterfaceTableRenderMixin, TestCase):
|
|
120
|
+
"""Test cases for DeviceModuleInterfaceTable."""
|
|
121
|
+
|
|
122
|
+
table_class = DeviceModuleInterfaceTable
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class InterfaceTemplateTableTestCase(TestCase):
|
|
126
|
+
"""Render tests for InterfaceTemplateTable speed/duplex columns."""
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def setUpTestData(cls):
|
|
130
|
+
manufacturer = Manufacturer.objects.create(name="Test Manuf Tmpl")
|
|
131
|
+
cls.device_type = DeviceType.objects.create(manufacturer=manufacturer, model="DT-Tmpl")
|
|
132
|
+
|
|
133
|
+
def test_render_speed_duplex_with_value(self):
|
|
134
|
+
interface_template = InterfaceTemplate.objects.create(
|
|
135
|
+
device_type=self.device_type,
|
|
136
|
+
name="tmpl-eth0",
|
|
137
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
138
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
139
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
140
|
+
)
|
|
141
|
+
table = InterfaceTemplateTable(InterfaceTemplate.objects.filter(pk=interface_template.pk))
|
|
142
|
+
bound_row = table.rows[0]
|
|
143
|
+
rendered_speed = bound_row.get_cell("speed") # pylint: disable=no-member
|
|
144
|
+
rendered_duplex = bound_row.get_cell("duplex") # pylint: disable=no-member
|
|
145
|
+
self.assertEqual(rendered_speed, "1 Gbps")
|
|
146
|
+
self.assertEqual(rendered_duplex, "Full")
|
|
147
|
+
|
|
148
|
+
def test_render_speed_duplex_with_none(self):
|
|
149
|
+
emdash = "\u2014"
|
|
150
|
+
interface_template = InterfaceTemplate.objects.create(
|
|
151
|
+
device_type=self.device_type,
|
|
152
|
+
name="tmpl-eth1",
|
|
153
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
154
|
+
)
|
|
155
|
+
table = InterfaceTemplateTable(InterfaceTemplate.objects.filter(pk=interface_template.pk))
|
|
156
|
+
bound_row = table.rows[0]
|
|
157
|
+
rendered_speed = bound_row.get_cell("speed") # pylint: disable=no-member
|
|
158
|
+
rendered_duplex = bound_row.get_cell("duplex") # pylint: disable=no-member
|
|
159
|
+
self.assertEqual(rendered_speed, emdash)
|
|
160
|
+
self.assertEqual(rendered_duplex, emdash)
|
|
@@ -30,6 +30,7 @@ from nautobot.dcim.choices import (
|
|
|
30
30
|
ConsolePortTypeChoices,
|
|
31
31
|
DeviceFaceChoices,
|
|
32
32
|
DeviceRedundancyGroupFailoverStrategyChoices,
|
|
33
|
+
InterfaceDuplexChoices,
|
|
33
34
|
InterfaceModeChoices,
|
|
34
35
|
InterfaceRedundancyGroupProtocolChoices,
|
|
35
36
|
InterfaceTypeChoices,
|
|
@@ -1837,6 +1838,68 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
|
|
1837
1838
|
"description": "new test description",
|
|
1838
1839
|
}
|
|
1839
1840
|
|
|
1841
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1842
|
+
def test_create_base_t_with_speed_and_duplex(self):
|
|
1843
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_devicetype")
|
|
1844
|
+
url = reverse("dcim:interfacetemplate_add")
|
|
1845
|
+
dt = DeviceType.objects.first()
|
|
1846
|
+
data = {
|
|
1847
|
+
"device_type": dt.pk,
|
|
1848
|
+
"name_pattern": "Eth-View-1",
|
|
1849
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
1850
|
+
"mgmt_only": False,
|
|
1851
|
+
"speed": 1_000_000,
|
|
1852
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
1853
|
+
"_create": True,
|
|
1854
|
+
}
|
|
1855
|
+
response = self.client.post(url, data)
|
|
1856
|
+
|
|
1857
|
+
# Successful create redirects (JobResult or object list)
|
|
1858
|
+
self.assertIn(response.status_code, (302, 303))
|
|
1859
|
+
interface_template = InterfaceTemplate.objects.get(name="Eth-View-1")
|
|
1860
|
+
self.assertEqual(interface_template.speed, 1_000_000)
|
|
1861
|
+
self.assertEqual(interface_template.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
|
|
1862
|
+
|
|
1863
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1864
|
+
def test_create_sfp_with_duplex_rejected(self):
|
|
1865
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_devicetype")
|
|
1866
|
+
url = reverse("dcim:interfacetemplate_add")
|
|
1867
|
+
dt = DeviceType.objects.first()
|
|
1868
|
+
data = {
|
|
1869
|
+
"device_type": dt.pk,
|
|
1870
|
+
"name_pattern": "SFP-View-1",
|
|
1871
|
+
"type": InterfaceTypeChoices.TYPE_1GE_SFP,
|
|
1872
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
1873
|
+
"_create": True,
|
|
1874
|
+
}
|
|
1875
|
+
response = self.client.post(url, data)
|
|
1876
|
+
# Form error returns 200 with field error displayed
|
|
1877
|
+
self.assertEqual(response.status_code, 200)
|
|
1878
|
+
content = response.content.decode(response.charset)
|
|
1879
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", content)
|
|
1880
|
+
|
|
1881
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1882
|
+
def test_bulk_create_with_speed_and_duplex(self):
|
|
1883
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_devicetype")
|
|
1884
|
+
url = reverse("dcim:interfacetemplate_add")
|
|
1885
|
+
dt = DeviceType.objects.first()
|
|
1886
|
+
data = {
|
|
1887
|
+
"device_type": dt.pk,
|
|
1888
|
+
"name_pattern": "Et[1-2]",
|
|
1889
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
1890
|
+
"mgmt_only": False,
|
|
1891
|
+
"speed": 1_000_000,
|
|
1892
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
1893
|
+
"_apply": True,
|
|
1894
|
+
}
|
|
1895
|
+
response = self.client.post(url, data)
|
|
1896
|
+
self.assertIn(response.status_code, (302, 303))
|
|
1897
|
+
objs = InterfaceTemplate.objects.filter(name__in=["Et1", "Et2"]).order_by("name")
|
|
1898
|
+
self.assertEqual(objs.count(), 2)
|
|
1899
|
+
for obj in objs:
|
|
1900
|
+
self.assertEqual(obj.speed, 1_000_000)
|
|
1901
|
+
self.assertEqual(obj.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
|
|
1902
|
+
|
|
1840
1903
|
|
|
1841
1904
|
class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
|
1842
1905
|
model = FrontPortTemplate
|
|
@@ -4811,7 +4874,7 @@ class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4811
4874
|
model = Controller
|
|
4812
4875
|
filterset = ControllerFilterSet
|
|
4813
4876
|
custom_action_required_permissions = {
|
|
4814
|
-
"dcim:
|
|
4877
|
+
"dcim:controller_wireless_networks": [
|
|
4815
4878
|
"dcim.view_controller",
|
|
4816
4879
|
"wireless.view_controllermanageddevicegroupwirelessnetworkassignment",
|
|
4817
4880
|
],
|