nautobot 2.4.21__py3-none-any.whl → 2.4.23__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.

Files changed (62) hide show
  1. nautobot/apps/choices.py +4 -0
  2. nautobot/apps/utils.py +8 -0
  3. nautobot/circuits/views.py +6 -2
  4. nautobot/core/cli/migrate_deprecated_templates.py +28 -9
  5. nautobot/core/filters.py +4 -0
  6. nautobot/core/forms/__init__.py +2 -0
  7. nautobot/core/forms/widgets.py +21 -2
  8. nautobot/core/jobs/bulk_actions.py +12 -6
  9. nautobot/core/jobs/cleanup.py +13 -1
  10. nautobot/core/settings.py +6 -0
  11. nautobot/core/settings_funcs.py +11 -1
  12. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  13. nautobot/core/templatetags/helpers.py +9 -7
  14. nautobot/core/tests/nautobot_config.py +3 -0
  15. nautobot/core/tests/test_jobs.py +118 -0
  16. nautobot/core/tests/test_templatetags_helpers.py +6 -0
  17. nautobot/core/tests/test_ui.py +49 -1
  18. nautobot/core/tests/test_utils.py +41 -1
  19. nautobot/core/ui/object_detail.py +7 -2
  20. nautobot/core/urls.py +7 -8
  21. nautobot/core/utils/filtering.py +11 -1
  22. nautobot/core/utils/lookup.py +46 -0
  23. nautobot/core/views/mixins.py +23 -17
  24. nautobot/core/views/utils.py +3 -3
  25. nautobot/dcim/api/serializers.py +3 -0
  26. nautobot/dcim/choices.py +49 -0
  27. nautobot/dcim/constants.py +7 -0
  28. nautobot/dcim/filters/__init__.py +7 -0
  29. nautobot/dcim/forms.py +89 -3
  30. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  31. nautobot/dcim/models/device_component_templates.py +33 -1
  32. nautobot/dcim/models/device_components.py +21 -0
  33. nautobot/dcim/tables/devices.py +14 -0
  34. nautobot/dcim/tables/devicetypes.py +8 -1
  35. nautobot/dcim/templates/dcim/interface.html +8 -0
  36. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  37. nautobot/dcim/tests/test_api.py +186 -6
  38. nautobot/dcim/tests/test_filters.py +32 -0
  39. nautobot/dcim/tests/test_forms.py +110 -8
  40. nautobot/dcim/tests/test_graphql.py +44 -1
  41. nautobot/dcim/tests/test_models.py +265 -0
  42. nautobot/dcim/tests/test_tables.py +160 -0
  43. nautobot/dcim/tests/test_views.py +64 -1
  44. nautobot/dcim/views.py +86 -77
  45. nautobot/extras/forms/forms.py +3 -1
  46. nautobot/extras/jobs.py +48 -2
  47. nautobot/extras/models/models.py +19 -0
  48. nautobot/extras/models/relationships.py +3 -1
  49. nautobot/extras/templates/extras/plugin_detail.html +2 -2
  50. nautobot/extras/urls.py +0 -14
  51. nautobot/extras/views.py +1 -1
  52. nautobot/ipam/ui.py +0 -17
  53. nautobot/ipam/views.py +2 -2
  54. nautobot/project-static/js/forms.js +92 -14
  55. nautobot/virtualization/tests/test_models.py +4 -2
  56. nautobot/virtualization/views.py +1 -0
  57. {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/METADATA +4 -4
  58. {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/RECORD +62 -59
  59. {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/LICENSE.txt +0 -0
  60. {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/NOTICE +0 -0
  61. {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/WHEEL +0 -0
  62. {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/entry_points.txt +0 -0
nautobot/dcim/forms.py CHANGED
@@ -27,7 +27,9 @@ from nautobot.core.forms import (
27
27
  form_from_model,
28
28
  JSONArrayFormField,
29
29
  MultipleContentTypeField,
30
+ MultiValueCharInput,
30
31
  NullableDateField,
32
+ NumberWithSelect,
31
33
  NumericArrayField,
32
34
  SelectWithPK,
33
35
  SmallTextarea,
@@ -37,7 +39,8 @@ from nautobot.core.forms import (
37
39
  )
38
40
  from nautobot.core.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
39
41
  from nautobot.core.forms.fields import LaxURLField
40
- from nautobot.dcim.constants import RACK_U_HEIGHT_MAXIMUM
42
+ from nautobot.core.utils.config import get_settings_or_config
43
+ from nautobot.dcim.constants import RACK_U_HEIGHT_DEFAULT, RACK_U_HEIGHT_MAXIMUM
41
44
  from nautobot.dcim.form_mixins import (
42
45
  LocatableModelBulkEditFormMixin,
43
46
  LocatableModelFilterFormMixin,
@@ -84,8 +87,10 @@ from .choices import (
84
87
  ControllerCapabilitiesChoices,
85
88
  DeviceFaceChoices,
86
89
  DeviceRedundancyGroupFailoverStrategyChoices,
90
+ InterfaceDuplexChoices,
87
91
  InterfaceModeChoices,
88
92
  InterfaceRedundancyGroupProtocolChoices,
93
+ InterfaceSpeedChoices,
89
94
  InterfaceTypeChoices,
90
95
  LocationDataToContactActionChoices,
91
96
  PortTypeChoices,
@@ -527,6 +532,17 @@ class RackForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm):
527
532
  )
528
533
  comments = CommentField()
529
534
 
535
+ def __init__(self, *args, **kwargs):
536
+ super().__init__(*args, **kwargs)
537
+ # Set initial value for u_height from Constance config when creating a new rack
538
+ if not self.instance.present_in_database and not kwargs.get("data"):
539
+ # Only set initial if this is a new form (not submitted data)
540
+ config_default = get_settings_or_config("RACK_DEFAULT_U_HEIGHT", fallback=RACK_U_HEIGHT_DEFAULT)
541
+ self.fields["u_height"].initial = config_default
542
+ # Override the form's initial dict to ensure it displays the Constance config value
543
+ # (unconditionally set it, even if already present from model default)
544
+ self.initial["u_height"] = config_default
545
+
530
546
  class Meta:
531
547
  model = Rack
532
548
  fields = [
@@ -1460,16 +1476,29 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
1460
1476
  "label",
1461
1477
  "type",
1462
1478
  "mgmt_only",
1479
+ "speed",
1480
+ "duplex",
1463
1481
  "description",
1464
1482
  ]
1465
1483
  widgets = {
1466
1484
  "type": StaticSelect2(),
1485
+ "speed": NumberWithSelect(choices=InterfaceSpeedChoices),
1486
+ "duplex": StaticSelect2(),
1487
+ }
1488
+ labels = {
1489
+ "speed": "Speed (Kbps)",
1467
1490
  }
1468
1491
 
1469
1492
 
1470
1493
  class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
1471
1494
  type = forms.ChoiceField(choices=InterfaceTypeChoices, widget=StaticSelect2())
1472
1495
  mgmt_only = forms.BooleanField(required=False, label="Management only")
1496
+ speed = forms.IntegerField(
1497
+ required=False, min_value=0, label="Speed (Kbps)", widget=NumberWithSelect(choices=InterfaceSpeedChoices)
1498
+ )
1499
+ duplex = forms.ChoiceField(
1500
+ choices=add_blank_choice(InterfaceDuplexChoices), required=False, widget=StaticSelect2(), label="Duplex"
1501
+ )
1473
1502
  field_order = (
1474
1503
  "device_type",
1475
1504
  "module_family",
@@ -1478,6 +1507,8 @@ class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
1478
1507
  "label_pattern",
1479
1508
  "type",
1480
1509
  "mgmt_only",
1510
+ "speed",
1511
+ "duplex",
1481
1512
  "description",
1482
1513
  )
1483
1514
 
@@ -1491,10 +1522,16 @@ class InterfaceTemplateBulkEditForm(NautobotBulkEditForm):
1491
1522
  widget=StaticSelect2(),
1492
1523
  )
1493
1524
  mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label="Management only")
1525
+ speed = forms.IntegerField(
1526
+ required=False, min_value=0, label="Speed (Kbps)", widget=NumberWithSelect(choices=InterfaceSpeedChoices)
1527
+ )
1528
+ duplex = forms.ChoiceField(
1529
+ choices=add_blank_choice(InterfaceDuplexChoices), required=False, widget=StaticSelect2(), label="Duplex"
1530
+ )
1494
1531
  description = forms.CharField(required=False)
1495
1532
 
1496
1533
  class Meta:
1497
- nullable_fields = ["label", "description"]
1534
+ nullable_fields = ["label", "speed", "duplex", "description"]
1498
1535
 
1499
1536
 
1500
1537
  class FrontPortTemplateForm(ModularComponentTemplateForm):
@@ -1965,6 +2002,8 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
1965
2002
  "label",
1966
2003
  "type",
1967
2004
  "mgmt_only",
2005
+ "speed",
2006
+ "duplex",
1968
2007
  ]
1969
2008
 
1970
2009
 
@@ -3131,6 +3170,7 @@ class PowerOutletBulkEditForm(
3131
3170
  class InterfaceFilterForm(ModularDeviceComponentFilterForm, RoleModelFilterFormMixin, StatusModelFilterFormMixin):
3132
3171
  model = Interface
3133
3172
  type = forms.MultipleChoiceField(choices=InterfaceTypeChoices, required=False, widget=StaticSelect2Multiple())
3173
+ speed = forms.MultipleChoiceField(choices=InterfaceSpeedChoices, required=False, widget=MultiValueCharInput)
3134
3174
  enabled = forms.NullBooleanField(required=False, widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES))
3135
3175
  mgmt_only = forms.NullBooleanField(required=False, widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES))
3136
3176
  mac_address = forms.CharField(required=False, label="MAC address")
@@ -3215,6 +3255,8 @@ class InterfaceForm(InterfaceCommonForm, ModularComponentEditForm):
3215
3255
  "bridge",
3216
3256
  "lag",
3217
3257
  "mac_address",
3258
+ "speed",
3259
+ "duplex",
3218
3260
  "ip_addresses",
3219
3261
  "virtual_device_contexts",
3220
3262
  "mtu",
@@ -3230,9 +3272,12 @@ class InterfaceForm(InterfaceCommonForm, ModularComponentEditForm):
3230
3272
  widgets = {
3231
3273
  "type": StaticSelect2(),
3232
3274
  "mode": StaticSelect2(),
3275
+ "speed": NumberWithSelect(choices=InterfaceSpeedChoices),
3276
+ "duplex": StaticSelect2(),
3233
3277
  }
3234
3278
  labels = {
3235
3279
  "mode": "802.1Q Mode",
3280
+ "speed": "Speed (Kbps)",
3236
3281
  }
3237
3282
  help_texts = {
3238
3283
  "mode": INTERFACE_MODE_HELP_TEXT,
@@ -3305,6 +3350,12 @@ class InterfaceCreateForm(ModularComponentCreateForm, InterfaceCommonForm, RoleN
3305
3350
  },
3306
3351
  )
3307
3352
  mac_address = forms.CharField(required=False, label="MAC Address")
3353
+ speed = forms.IntegerField(
3354
+ required=False, min_value=0, label="Speed (Kbps)", widget=NumberWithSelect(choices=InterfaceSpeedChoices)
3355
+ )
3356
+ duplex = forms.ChoiceField(
3357
+ choices=add_blank_choice(InterfaceDuplexChoices), required=False, widget=StaticSelect2(), label="Duplex"
3358
+ )
3308
3359
  mgmt_only = forms.BooleanField(
3309
3360
  required=False,
3310
3361
  label="Management only",
@@ -3351,6 +3402,8 @@ class InterfaceCreateForm(ModularComponentCreateForm, InterfaceCommonForm, RoleN
3351
3402
  "status",
3352
3403
  "role",
3353
3404
  "type",
3405
+ "speed",
3406
+ "duplex",
3354
3407
  "enabled",
3355
3408
  "parent_interface",
3356
3409
  "bridge",
@@ -3384,6 +3437,10 @@ class InterfaceBulkCreateForm(
3384
3437
  queryset=Status.objects.all(),
3385
3438
  query_params={"content_types": Interface._meta.label_lower},
3386
3439
  )
3440
+ speed = forms.IntegerField(required=False, min_value=0, label="Speed (Kbps)")
3441
+ duplex = forms.ChoiceField(
3442
+ choices=add_blank_choice(InterfaceDuplexChoices), required=False, widget=StaticSelect2(), label="Duplex"
3443
+ )
3387
3444
 
3388
3445
  field_order = (
3389
3446
  "name_pattern",
@@ -3397,6 +3454,8 @@ class InterfaceBulkCreateForm(
3397
3454
  "mgmt_only",
3398
3455
  "description",
3399
3456
  "mode",
3457
+ "speed",
3458
+ "duplex",
3400
3459
  "tags",
3401
3460
  )
3402
3461
 
@@ -3416,6 +3475,10 @@ class ModuleInterfaceBulkCreateForm(
3416
3475
  queryset=Status.objects.all(),
3417
3476
  query_params={"content_types": Interface._meta.label_lower},
3418
3477
  )
3478
+ speed = forms.IntegerField(required=False, min_value=0, label="Speed (Kbps)")
3479
+ duplex = forms.ChoiceField(
3480
+ choices=add_blank_choice(InterfaceDuplexChoices), required=False, widget=StaticSelect2(), label="Duplex"
3481
+ )
3419
3482
 
3420
3483
  field_order = (
3421
3484
  "name_pattern",
@@ -3429,13 +3492,28 @@ class ModuleInterfaceBulkCreateForm(
3429
3492
  "mgmt_only",
3430
3493
  "description",
3431
3494
  "mode",
3495
+ "speed",
3496
+ "duplex",
3432
3497
  "tags",
3433
3498
  )
3434
3499
 
3435
3500
 
3436
3501
  class InterfaceBulkEditForm(
3437
3502
  form_from_model(
3438
- Interface, ["label", "type", "parent_interface", "bridge", "lag", "mac_address", "mtu", "description", "mode"]
3503
+ Interface,
3504
+ [
3505
+ "label",
3506
+ "type",
3507
+ "parent_interface",
3508
+ "bridge",
3509
+ "lag",
3510
+ "mac_address",
3511
+ "mtu",
3512
+ "description",
3513
+ "mode",
3514
+ "speed",
3515
+ "duplex",
3516
+ ],
3439
3517
  ),
3440
3518
  TagsBulkEditFormMixin,
3441
3519
  StatusModelBulkEditFormMixin,
@@ -3479,6 +3557,12 @@ class InterfaceBulkEditForm(
3479
3557
  label="VRF",
3480
3558
  required=False,
3481
3559
  )
3560
+ speed = forms.IntegerField(
3561
+ required=False, min_value=0, label="Speed (Kbps)", widget=NumberWithSelect(choices=InterfaceSpeedChoices)
3562
+ )
3563
+ duplex = forms.ChoiceField(
3564
+ choices=add_blank_choice(InterfaceDuplexChoices), required=False, widget=StaticSelect2(), label="Duplex"
3565
+ )
3482
3566
 
3483
3567
  class Meta:
3484
3568
  nullable_fields = [
@@ -3490,6 +3574,8 @@ class InterfaceBulkEditForm(
3490
3574
  "mtu",
3491
3575
  "description",
3492
3576
  "mode",
3577
+ "speed",
3578
+ "duplex",
3493
3579
  "untagged_vlan",
3494
3580
  "tagged_vlans",
3495
3581
  "vrf",
@@ -0,0 +1,32 @@
1
+ # Generated by Django 4.2.25 on 2025-11-01 22:09
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("dcim", "0074_alter_rack_u_height"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="interface",
14
+ name="duplex",
15
+ field=models.CharField(blank=True, default="", max_length=10),
16
+ ),
17
+ migrations.AddField(
18
+ model_name="interface",
19
+ name="speed",
20
+ field=models.PositiveIntegerField(blank=True, null=True),
21
+ ),
22
+ migrations.AddField(
23
+ model_name="interfacetemplate",
24
+ name="duplex",
25
+ field=models.CharField(blank=True, default="", max_length=10),
26
+ ),
27
+ migrations.AddField(
28
+ model_name="interfacetemplate",
29
+ name="speed",
30
+ field=models.PositiveIntegerField(blank=True, null=True),
31
+ ),
32
+ ]
@@ -11,6 +11,7 @@ from nautobot.core.models.fields import ForeignKeyWithAutoRelatedName, NaturalOr
11
11
  from nautobot.core.models.ordering import naturalize_interface
12
12
  from nautobot.dcim.choices import (
13
13
  ConsolePortTypeChoices,
14
+ InterfaceDuplexChoices,
14
15
  InterfaceTypeChoices,
15
16
  PortTypeChoices,
16
17
  PowerOutletFeedLegChoices,
@@ -18,7 +19,13 @@ from nautobot.dcim.choices import (
18
19
  PowerPortTypeChoices,
19
20
  SubdeviceRoleChoices,
20
21
  )
21
- from nautobot.dcim.constants import REARPORT_POSITIONS_MAX, REARPORT_POSITIONS_MIN
22
+ from nautobot.dcim.constants import (
23
+ COPPER_TWISTED_PAIR_IFACE_TYPES,
24
+ REARPORT_POSITIONS_MAX,
25
+ REARPORT_POSITIONS_MIN,
26
+ VIRTUAL_IFACE_TYPES,
27
+ WIRELESS_IFACE_TYPES,
28
+ )
22
29
  from nautobot.extras.models import (
23
30
  ChangeLoggedModel,
24
31
  ContactMixin,
@@ -349,6 +356,29 @@ class InterfaceTemplate(ModularComponentTemplateModel):
349
356
  )
350
357
  type = models.CharField(max_length=50, choices=InterfaceTypeChoices)
351
358
  mgmt_only = models.BooleanField(default=False, verbose_name="Management only")
359
+ speed = models.PositiveIntegerField(null=True, blank=True)
360
+ duplex = models.CharField(max_length=10, choices=InterfaceDuplexChoices, blank=True, default="")
361
+
362
+ def clean(self):
363
+ super().clean()
364
+ self._validate_speed_and_duplex()
365
+
366
+ def _validate_speed_and_duplex(self):
367
+ """Validate speed (Kbps) and duplex based on interface type."""
368
+
369
+ is_lag = self.type == InterfaceTypeChoices.TYPE_LAG
370
+ is_virtual = self.type in VIRTUAL_IFACE_TYPES
371
+ is_wireless = self.type in WIRELESS_IFACE_TYPES
372
+
373
+ # Check settings by interface type
374
+ if self.speed and any([is_lag, is_virtual, is_wireless]):
375
+ raise ValidationError({"speed": "Speed is not applicable to this interface type."})
376
+
377
+ if self.duplex and any([is_lag, is_virtual, is_wireless]):
378
+ raise ValidationError({"duplex": "Duplex is not applicable to this interface type."})
379
+
380
+ if self.duplex and self.type not in COPPER_TWISTED_PAIR_IFACE_TYPES:
381
+ raise ValidationError({"duplex": "Duplex is only applicable to copper twisted-pair interfaces."})
352
382
 
353
383
  def instantiate(self, device, module=None):
354
384
  try:
@@ -361,6 +391,8 @@ class InterfaceTemplate(ModularComponentTemplateModel):
361
391
  module=module,
362
392
  type=self.type,
363
393
  mgmt_only=self.mgmt_only,
394
+ speed=self.speed,
395
+ duplex=self.duplex,
364
396
  status=status,
365
397
  )
366
398
 
@@ -19,6 +19,7 @@ from nautobot.core.models.tree_queries import TreeModel
19
19
  from nautobot.core.utils.data import UtilizationData
20
20
  from nautobot.dcim.choices import (
21
21
  ConsolePortTypeChoices,
22
+ InterfaceDuplexChoices,
22
23
  InterfaceModeChoices,
23
24
  InterfaceRedundancyGroupProtocolChoices,
24
25
  InterfaceStatusChoices,
@@ -31,6 +32,7 @@ from nautobot.dcim.choices import (
31
32
  SubdeviceRoleChoices,
32
33
  )
33
34
  from nautobot.dcim.constants import (
35
+ COPPER_TWISTED_PAIR_IFACE_TYPES,
34
36
  NONCONNECTABLE_IFACE_TYPES,
35
37
  REARPORT_POSITIONS_MAX,
36
38
  REARPORT_POSITIONS_MIN,
@@ -743,6 +745,9 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
743
745
  blank=True,
744
746
  verbose_name="IP Addresses",
745
747
  )
748
+ # Operational attributes (distinct from interface type capabilities)
749
+ speed = models.PositiveIntegerField(null=True, blank=True)
750
+ duplex = models.CharField(max_length=10, choices=InterfaceDuplexChoices, blank=True, default="")
746
751
 
747
752
  class Meta(ModularComponentModel.Meta):
748
753
  ordering = ("device", "module__id", CollateAsChar("_name")) # Module.ordering is complex; don't order by module
@@ -877,6 +882,22 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
877
882
  }
878
883
  )
879
884
 
885
+ # Speed/Duplex validation
886
+ self._validate_speed_and_duplex()
887
+
888
+ def _validate_speed_and_duplex(self):
889
+ """Validate speed (Kbps) and duplex based on interface type."""
890
+
891
+ # Check settings by interface type
892
+ if self.speed and any([self.is_lag, self.is_virtual, self.is_wireless]):
893
+ raise ValidationError({"speed": "Speed is not applicable to this interface type."})
894
+
895
+ if self.duplex and any([self.is_lag, self.is_virtual, self.is_wireless]):
896
+ raise ValidationError({"duplex": "Duplex is not applicable to this interface type."})
897
+
898
+ if self.duplex and self.type not in COPPER_TWISTED_PAIR_IFACE_TYPES:
899
+ raise ValidationError({"duplex": "Duplex is only applicable to copper twisted-pair interfaces."})
900
+
880
901
  @property
881
902
  def is_connectable(self):
882
903
  return self.type not in NONCONNECTABLE_IFACE_TYPES
@@ -11,6 +11,7 @@ from nautobot.core.tables import (
11
11
  TagColumn,
12
12
  ToggleColumn,
13
13
  )
14
+ from nautobot.core.templatetags.helpers import humanize_speed
14
15
  from nautobot.dcim.models import (
15
16
  ConsolePort,
16
17
  ConsoleServerPort,
@@ -724,6 +725,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
724
725
  url_params={"interfaces": "pk"},
725
726
  verbose_name="Virtual Device Contexts",
726
727
  )
728
+ speed = tables.Column(verbose_name="Speed", accessor="speed", orderable=True)
729
+ duplex = tables.Column(verbose_name="Duplex", accessor="duplex", orderable=True)
727
730
 
728
731
  class Meta(ModularDeviceComponentTable.Meta):
729
732
  model = Interface
@@ -737,6 +740,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
737
740
  "label",
738
741
  "enabled",
739
742
  "type",
743
+ "speed",
744
+ "duplex",
740
745
  "mgmt_only",
741
746
  "mtu",
742
747
  "vrf",
@@ -762,9 +767,13 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
762
767
  "label",
763
768
  "enabled",
764
769
  "type",
770
+ "speed",
765
771
  "description",
766
772
  )
767
773
 
774
+ def render_speed(self, record):
775
+ return humanize_speed(record.speed)
776
+
768
777
 
769
778
  class DeviceModuleInterfaceTable(InterfaceTable):
770
779
  name = tables.TemplateColumn(
@@ -790,6 +799,8 @@ class DeviceModuleInterfaceTable(InterfaceTable):
790
799
  "module",
791
800
  "enabled",
792
801
  "type",
802
+ "speed",
803
+ "duplex",
793
804
  "parent_interface",
794
805
  "bridge",
795
806
  "lag",
@@ -835,6 +846,9 @@ class DeviceModuleInterfaceTable(InterfaceTable):
835
846
  "data-name": lambda record: record.name,
836
847
  }
837
848
 
849
+ def render_speed(self, record):
850
+ return humanize_speed(record.speed)
851
+
838
852
 
839
853
  class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
840
854
  rear_port_position = tables.Column(verbose_name="Position")
@@ -8,6 +8,7 @@ from nautobot.core.tables import (
8
8
  TagColumn,
9
9
  ToggleColumn,
10
10
  )
11
+ from nautobot.core.templatetags.helpers import humanize_speed
11
12
  from nautobot.dcim.models import (
12
13
  ConsolePortTemplate,
13
14
  ConsoleServerPortTemplate,
@@ -270,6 +271,8 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
270
271
 
271
272
  class InterfaceTemplateTable(ComponentTemplateTable):
272
273
  mgmt_only = BooleanColumn(verbose_name="Management Only")
274
+ speed = tables.Column(verbose_name="Speed", accessor="speed", orderable=True)
275
+ duplex = tables.Column(verbose_name="Duplex", accessor="duplex", orderable=True)
273
276
  actions = ButtonsColumn(
274
277
  model=InterfaceTemplate,
275
278
  buttons=("edit", "delete"),
@@ -278,9 +281,13 @@ class InterfaceTemplateTable(ComponentTemplateTable):
278
281
 
279
282
  class Meta(BaseTable.Meta):
280
283
  model = InterfaceTemplate
281
- fields = ("pk", "name", "label", "mgmt_only", "type", "description", "actions")
284
+ fields = ("pk", "name", "label", "mgmt_only", "type", "speed", "duplex", "description", "actions")
285
+ default_columns = ("pk", "name", "label", "mgmt_only", "type", "speed", "description", "actions")
282
286
  empty_text = "None"
283
287
 
288
+ def render_speed(self, record):
289
+ return humanize_speed(record.speed)
290
+
284
291
 
285
292
  class FrontPortTemplateTable(ComponentTemplateTable):
286
293
  rear_port_position = tables.Column(verbose_name="Position")
@@ -44,6 +44,14 @@
44
44
  <td>Type</td>
45
45
  <td>{{ object.get_type_display }}</td>
46
46
  </tr>
47
+ <tr>
48
+ <td>Speed</td>
49
+ <td>{{ object.speed|humanize_speed|placeholder }}</td>
50
+ </tr>
51
+ <tr>
52
+ <td>Duplex</td>
53
+ <td>{{ object.get_duplex_display|placeholder }}</td>
54
+ </tr>
47
55
  <tr>
48
56
  <td>Enabled</td>
49
57
  <td>{{ object.enabled | render_boolean }}</td>
@@ -12,6 +12,8 @@
12
12
  {% render_field form.status %}
13
13
  {% render_field form.role %}
14
14
  {% render_field form.type %}
15
+ {% render_field form.speed %}
16
+ {% render_field form.duplex %}
15
17
  {% render_field form.enabled %}
16
18
  {% render_field form.parent_interface %}
17
19
  {% render_field form.bridge %}