Bläddra i källkod

Closes #1099: Add PoE mode & type for interfaces

jeremystretch 3 år sedan
förälder
incheckning
4587b83d85

+ 4 - 0
docs/models/dcim/interface.md

@@ -11,6 +11,10 @@ Interfaces may be physical or virtual in nature, but only physical interfaces ma
 
 
 Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
 Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
 
 
+### Power over Ethernet (PoE)
+
+Physical interfaces can be assigned a PoE mode to indicate PoE capability: power supplying equipment (PSE) or powered device (PD). Additionally, a PoE mode may be specified. This can be one of the listed IEEE 802.3 standards, or a passive setting (24 or 48 volts across two or four pairs).
+
 ### Wireless Interfaces
 ### Wireless Interfaces
 
 
 Wireless interfaces may additionally track the following attributes:
 Wireless interfaces may additionally track the following attributes:

+ 4 - 0
docs/release-notes/version-3.3.md

@@ -11,6 +11,8 @@
 
 
 #### Half-Height Rack Units ([#51](https://github.com/netbox-community/netbox/issues/51))
 #### Half-Height Rack Units ([#51](https://github.com/netbox-community/netbox/issues/51))
 
 
+#### PoE Interface Attributes ([#1099](https://github.com/netbox-community/netbox/issues/1099))
+
 ### Enhancements
 ### Enhancements
 
 
 * [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
 * [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
@@ -33,6 +35,8 @@
     * The `position` field has been changed from an integer to a decimal
     * The `position` field has been changed from an integer to a decimal
 * dcim.DeviceType
 * dcim.DeviceType
     * The `u_height` field has been changed from an integer to a decimal
     * The `u_height` field has been changed from an integer to a decimal
+* dcim.Interface
+    * Added the option `poe_mode` and `poe_type` fields
 * dcim.Rack
 * dcim.Rack
     * The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
     * The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
 * extras.CustomField
 * extras.CustomField

+ 6 - 4
netbox/dcim/api/serializers.py

@@ -812,6 +812,8 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
     duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
     duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
     rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
     rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
     rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
     rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
+    poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
+    poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     tagged_vlans = SerializedPKRelatedField(
     tagged_vlans = SerializedPKRelatedField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
@@ -836,10 +838,10 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
         fields = [
         fields = [
             'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
             'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
             'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
             'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
-            'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected',
-            'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'vrf', 'connected_endpoint',
-            'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
-            'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
+            'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
+            'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans',
+            'vrf', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
+            'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
         ]
         ]
 
 
     def validate(self, data):
     def validate(self, data):

+ 45 - 0
netbox/dcim/choices.py

@@ -1003,6 +1003,51 @@ class InterfaceModeChoices(ChoiceSet):
     )
     )
 
 
 
 
+class InterfacePoEModeChoices(ChoiceSet):
+
+    MODE_PD = 'pd'
+    MODE_PSE = 'pse'
+
+    CHOICES = (
+        (MODE_PD, 'Powered device (PD)'),
+        (MODE_PSE, 'Power sourcing equipment (PSE)'),
+    )
+
+
+class InterfacePoETypeChoices(ChoiceSet):
+
+    TYPE_1_8023AF = 'type1-ieee802.3af'
+    TYPE_2_8023AT = 'type2-ieee802.3at'
+    TYPE_3_8023BT = 'type3-ieee802.3bt'
+    TYPE_4_8023BT = 'type4-ieee802.3bt'
+
+    PASSIVE_24V_2PAIR = 'passive-24v-2pair'
+    PASSIVE_24V_4PAIR = 'passive-24v-4pair'
+    PASSIVE_48V_2PAIR = 'passive-48v-2pair'
+    PASSIVE_48V_4PAIR = 'passive-48v-4pair'
+
+    CHOICES = (
+        (
+            'IEEE Standard',
+            (
+                (TYPE_1_8023AF, '802.3af (Type 1)'),
+                (TYPE_2_8023AT, '802.3at (Type 2)'),
+                (TYPE_3_8023BT, '802.3bt (Type 3)'),
+                (TYPE_4_8023BT, '802.3bt (Type 4)'),
+            )
+        ),
+        (
+            'Passive',
+            (
+                (PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
+                (PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
+                (PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
+                (PASSIVE_48V_2PAIR, 'Passive 48V (4-pair)'),
+            )
+        ),
+    )
+
+
 #
 #
 # FrontPorts/RearPorts
 # FrontPorts/RearPorts
 #
 #

+ 8 - 2
netbox/dcim/filtersets.py

@@ -1238,6 +1238,12 @@ class InterfaceFilterSet(
     )
     )
     mac_address = MultiValueMACAddressFilter()
     mac_address = MultiValueMACAddressFilter()
     wwn = MultiValueWWNFilter()
     wwn = MultiValueWWNFilter()
+    poe_mode = django_filters.MultipleChoiceFilter(
+        choices=InterfacePoEModeChoices
+    )
+    poe_type = django_filters.MultipleChoiceFilter(
+        choices=InterfacePoETypeChoices
+    )
     vlan_id = django_filters.CharFilter(
     vlan_id = django_filters.CharFilter(
         method='filter_vlan_id',
         method='filter_vlan_id',
         label='Assigned VLAN'
         label='Assigned VLAN'
@@ -1271,8 +1277,8 @@ class InterfaceFilterSet(
     class Meta:
     class Meta:
         model = Interface
         model = Interface
         fields = [
         fields = [
-            'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel',
-            'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
+            'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
+            'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
         ]
         ]
 
 
     def filter_device(self, queryset, name, value):
     def filter_device(self, queryset, name, value):

+ 5 - 2
netbox/dcim/forms/bulk_create.py

@@ -72,12 +72,15 @@ class PowerOutletBulkCreateForm(
 
 
 
 
 class InterfaceBulkCreateForm(
 class InterfaceBulkCreateForm(
-    form_from_model(Interface, ['type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected']),
+    form_from_model(Interface, [
+        'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type',
+    ]),
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     model = Interface
     model = Interface
     field_order = (
     field_order = (
-        'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
+        'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
+        'poe_type', 'mark_connected', 'description', 'tags',
     )
     )
 
 
 
 

+ 15 - 2
netbox/dcim/forms/bulk_edit.py

@@ -1063,6 +1063,18 @@ class InterfaceBulkEditForm(
         widget=BulkEditNullBooleanSelect,
         widget=BulkEditNullBooleanSelect,
         label='Management only'
         label='Management only'
     )
     )
+    poe_mode = forms.ChoiceField(
+        choices=add_blank_choice(InterfacePoEModeChoices),
+        required=False,
+        initial='',
+        widget=StaticSelect()
+    )
+    poe_type = forms.ChoiceField(
+        choices=add_blank_choice(InterfacePoETypeChoices),
+        required=False,
+        initial='',
+        widget=StaticSelect()
+    )
     mark_connected = forms.NullBooleanField(
     mark_connected = forms.NullBooleanField(
         required=False,
         required=False,
         widget=BulkEditNullBooleanSelect
         widget=BulkEditNullBooleanSelect
@@ -1105,14 +1117,15 @@ class InterfaceBulkEditForm(
         (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
         (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
+        ('PoE', ('poe_mode', 'poe_type')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
         ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
         ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
         ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
         ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
         'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
-        'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'vlan_group', 'untagged_vlan',
-        'tagged_vlans', 'vrf',
+        'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
+        'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
     )
     )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 13 - 3
netbox/dcim/forms/bulk_import.py

@@ -622,6 +622,16 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
         choices=InterfaceDuplexChoices,
         choices=InterfaceDuplexChoices,
         required=False
         required=False
     )
     )
+    poe_mode = CSVChoiceField(
+        choices=InterfacePoEModeChoices,
+        required=False,
+        help_text='PoE mode'
+    )
+    poe_type = CSVChoiceField(
+        choices=InterfacePoETypeChoices,
+        required=False,
+        help_text='PoE type'
+    )
     mode = CSVChoiceField(
     mode = CSVChoiceField(
         choices=InterfaceModeChoices,
         choices=InterfaceModeChoices,
         required=False,
         required=False,
@@ -642,9 +652,9 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
     class Meta:
     class Meta:
         model = Interface
         model = Interface
         fields = (
         fields = (
-            'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'mark_connected', 'mac_address',
-            'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency',
-            'rf_channel_width', 'tx_power',
+            'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
+            'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
+            'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
         )
         )
 
 
     def __init__(self, data=None, *args, **kwargs):
     def __init__(self, data=None, *args, **kwargs):

+ 9 - 0
netbox/dcim/forms/filtersets.py

@@ -969,6 +969,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
         (None, ('q', 'tag')),
         (None, ('q', 'tag')),
         ('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
         ('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
         ('Addressing', ('vrf_id', 'mac_address', 'wwn')),
         ('Addressing', ('vrf_id', 'mac_address', 'wwn')),
+        ('PoE', ('poe_mode', 'poe_type')),
         ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
         ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
         ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
         ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
     )
     )
@@ -1009,6 +1010,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
         required=False,
         required=False,
         label='WWN'
         label='WWN'
     )
     )
+    poe_mode = MultipleChoiceField(
+        choices=InterfacePoEModeChoices,
+        required=False
+    )
+    poe_type = MultipleChoiceField(
+        choices=InterfacePoEModeChoices,
+        required=False
+    )
     rf_role = MultipleChoiceField(
     rf_role = MultipleChoiceField(
         choices=WirelessRoleChoices,
         choices=WirelessRoleChoices,
         required=False,
         required=False,

+ 6 - 3
netbox/dcim/forms/models.py

@@ -1314,6 +1314,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
+        ('PoE', ('poe_mode', 'poe_type')),
         ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
         ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
         ('Wireless', (
         ('Wireless', (
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
@@ -1324,14 +1325,16 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
         model = Interface
         model = Interface
         fields = [
         fields = [
             'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
             'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
-            'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel',
-            'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans',
-            'vrf', 'tags',
+            'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
+            'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
+            'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'device': forms.HiddenInput(),
             'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'speed': SelectSpeedWidget(),
             'speed': SelectSpeedWidget(),
+            'poe_mode': StaticSelect(),
+            'poe_type': StaticSelect(),
             'duplex': StaticSelect(),
             'duplex': StaticSelect(),
             'mode': StaticSelect(),
             'mode': StaticSelect(),
             'rf_role': StaticSelect(),
             'rf_role': StaticSelect(),

+ 6 - 0
netbox/dcim/graphql/types.py

@@ -226,6 +226,12 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType):
         exclude = ('_path',)
         exclude = ('_path',)
         filterset_class = filtersets.InterfaceFilterSet
         filterset_class = filtersets.InterfaceFilterSet
 
 
+    def resolve_poe_mode(self, info):
+        return self.poe_mode or None
+
+    def resolve_poe_type(self, info):
+        return self.poe_type or None
+
     def resolve_mode(self, info):
     def resolve_mode(self, info):
         return self.mode or None
         return self.mode or None
 
 

+ 23 - 0
netbox/dcim/migrations/0155_interface_poe_mode_type.py

@@ -0,0 +1,23 @@
+# Generated by Django 4.0.5 on 2022-06-22 00:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0154_half_height_rack_units'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='interface',
+            name='poe_mode',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='poe_type',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+    ]

+ 31 - 1
netbox/dcim/models/device_components.py

@@ -590,6 +590,18 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
         validators=(MaxValueValidator(127),),
         validators=(MaxValueValidator(127),),
         verbose_name='Transmit power (dBm)'
         verbose_name='Transmit power (dBm)'
     )
     )
+    poe_mode = models.CharField(
+        max_length=50,
+        choices=InterfacePoEModeChoices,
+        blank=True,
+        verbose_name='PoE mode'
+    )
+    poe_type = models.CharField(
+        max_length=50,
+        choices=InterfacePoETypeChoices,
+        blank=True,
+        verbose_name='PoE type'
+    )
     wireless_link = models.ForeignKey(
     wireless_link = models.ForeignKey(
         to='wireless.WirelessLink',
         to='wireless.WirelessLink',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
@@ -638,7 +650,7 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
         related_query_name='+'
         related_query_name='+'
     )
     )
 
 
-    clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']
+    clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'poe_mode', 'poe_type']
 
 
     class Meta:
     class Meta:
         ordering = ('device', CollateAsChar('_name'))
         ordering = ('device', CollateAsChar('_name'))
@@ -726,6 +738,24 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
                            f"of virtual chassis {self.device.virtual_chassis}."
                            f"of virtual chassis {self.device.virtual_chassis}."
                 })
                 })
 
 
+        # PoE validation
+
+        # Only physical interfaces may have a PoE mode/type assigned
+        if self.poe_mode and self.is_virtual:
+            raise ValidationError({
+                'poe_mode': "Virtual interfaces cannot have a PoE mode."
+            })
+        if self.poe_type and self.is_virtual:
+            raise ValidationError({
+                'poe_type': "Virtual interfaces cannot have a PoE type."
+            })
+
+        # An interface with a PoE type set must also specify a mode
+        if self.poe_type and not self.poe_mode:
+            raise ValidationError({
+                'poe_type': "Must specify PoE mode when designating a PoE type."
+            })
+
         # Wireless validation
         # Wireless validation
 
 
         # RF role & channel may only be set for wireless interfaces
         # RF role & channel may only be set for wireless interfaces

+ 4 - 4
netbox/dcim/tables/devices.py

@@ -520,10 +520,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
         model = Interface
         model = Interface
         fields = (
         fields = (
             'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
             'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
-            'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
-            'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
-            'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
-            'tagged_vlans', 'created', 'last_updated',
+            'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
+            'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
+            'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses',
+            'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
         default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
 
 

+ 2 - 0
netbox/dcim/tests/test_api.py

@@ -1507,6 +1507,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
                 'speed': 1000000,
                 'speed': 1000000,
                 'duplex': 'full',
                 'duplex': 'full',
                 'vrf': vrfs[0].pk,
                 'vrf': vrfs[0].pk,
+                'poe_mode': InterfacePoEModeChoices.MODE_PD,
+                'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'untagged_vlan': vlans[2].pk,
             },
             },

+ 111 - 8
netbox/dcim/tests/test_filtersets.py

@@ -2540,14 +2540,109 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
         Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
 
 
         interfaces = (
         interfaces = (
-            Interface(device=devices[0], module=modules[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First', vrf=vrfs[0], speed=1000000, duplex='half'),
-            Interface(device=devices[1], module=modules[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second', vrf=vrfs[1], speed=1000000, duplex='full'),
-            Interface(device=devices[2], module=modules[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third', vrf=vrfs[2], speed=100000, duplex='half'),
-            Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40, speed=100000, duplex='full'),
-            Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
-            Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, tx_power=40),
-            Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22),
-            Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_STATION, rf_channel=WirelessChannelChoices.CHANNEL_5G_32, rf_channel_frequency=5160, rf_channel_width=20),
+            Interface(
+                device=devices[0],
+                module=modules[0],
+                name='Interface 1',
+                label='A',
+                type=InterfaceTypeChoices.TYPE_1GE_SFP,
+                enabled=True,
+                mgmt_only=True,
+                mtu=100,
+                mode=InterfaceModeChoices.MODE_ACCESS,
+                mac_address='00-00-00-00-00-01',
+                description='First',
+                vrf=vrfs[0],
+                speed=1000000,
+                duplex='half',
+                poe_mode=InterfacePoEModeChoices.MODE_PSE,
+                poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
+            ),
+            Interface(
+                device=devices[1],
+                module=modules[1],
+                name='Interface 2',
+                label='B',
+                type=InterfaceTypeChoices.TYPE_1GE_GBIC,
+                enabled=True,
+                mgmt_only=True,
+                mtu=200,
+                mode=InterfaceModeChoices.MODE_TAGGED,
+                mac_address='00-00-00-00-00-02',
+                description='Second',
+                vrf=vrfs[1],
+                speed=1000000,
+                duplex='full',
+                poe_mode=InterfacePoEModeChoices.MODE_PD,
+                poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
+            ),
+            Interface(
+                device=devices[2],
+                module=modules[2],
+                name='Interface 3',
+                label='C',
+                type=InterfaceTypeChoices.TYPE_1GE_FIXED,
+                enabled=False,
+                mgmt_only=False,
+                mtu=300,
+                mode=InterfaceModeChoices.MODE_TAGGED_ALL,
+                mac_address='00-00-00-00-00-03',
+                description='Third',
+                vrf=vrfs[2],
+                speed=100000,
+                duplex='half',
+                poe_mode=InterfacePoEModeChoices.MODE_PSE,
+                poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
+            ),
+            Interface(
+                device=devices[3],
+                name='Interface 4',
+                label='D',
+                type=InterfaceTypeChoices.TYPE_OTHER,
+                enabled=True,
+                mgmt_only=True,
+                tx_power=40,
+                speed=100000,
+                duplex='full',
+                poe_mode=InterfacePoEModeChoices.MODE_PD,
+                poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
+            ),
+            Interface(
+                device=devices[3],
+                name='Interface 5',
+                label='E',
+                type=InterfaceTypeChoices.TYPE_OTHER,
+                enabled=True,
+                mgmt_only=True,
+                tx_power=40
+            ),
+            Interface(
+                device=devices[3],
+                name='Interface 6',
+                label='F',
+                type=InterfaceTypeChoices.TYPE_OTHER,
+                enabled=False,
+                mgmt_only=False,
+                tx_power=40
+            ),
+            Interface(
+                device=devices[3],
+                name='Interface 7',
+                type=InterfaceTypeChoices.TYPE_80211AC,
+                rf_role=WirelessRoleChoices.ROLE_AP,
+                rf_channel=WirelessChannelChoices.CHANNEL_24G_1,
+                rf_channel_frequency=2412,
+                rf_channel_width=22
+            ),
+            Interface(
+                device=devices[3],
+                name='Interface 8',
+                type=InterfaceTypeChoices.TYPE_80211AC,
+                rf_role=WirelessRoleChoices.ROLE_STATION,
+                rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
+                rf_channel_frequency=5160,
+                rf_channel_width=20
+            ),
         )
         )
         Interface.objects.bulk_create(interfaces)
         Interface.objects.bulk_create(interfaces)
 
 
@@ -2594,6 +2689,14 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'mgmt_only': 'false'}
         params = {'mgmt_only': 'false'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
+    def test_poe_mode(self):
+        params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+    def test_poe_type(self):
+        params = {'poe_type': [InterfacePoETypeChoices.TYPE_1_8023AF, InterfacePoETypeChoices.TYPE_2_8023AT]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
     def test_mode(self):
     def test_mode(self):
         params = {'mode': InterfaceModeChoices.MODE_ACCESS}
         params = {'mode': InterfaceModeChoices.MODE_ACCESS}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)

+ 10 - 4
netbox/dcim/tests/test_views.py

@@ -2204,6 +2204,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'description': 'A front port',
             'description': 'A front port',
             'mode': InterfaceModeChoices.MODE_TAGGED,
             'mode': InterfaceModeChoices.MODE_TAGGED,
             'tx_power': 10,
             'tx_power': 10,
+            'poe_mode': InterfacePoEModeChoices.MODE_PSE,
+            'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
             'untagged_vlan': vlans[0].pk,
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
@@ -2225,6 +2227,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'duplex': 'half',
             'duplex': 'half',
             'mgmt_only': True,
             'mgmt_only': True,
             'description': 'A front port',
             'description': 'A front port',
+            'poe_mode': InterfacePoEModeChoices.MODE_PSE,
+            'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
             'mode': InterfaceModeChoices.MODE_TAGGED,
             'mode': InterfaceModeChoices.MODE_TAGGED,
             'untagged_vlan': vlans[0].pk,
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'tagged_vlans': [v.pk for v in vlans[1:4]],
@@ -2244,6 +2248,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'duplex': 'full',
             'duplex': 'full',
             'mgmt_only': True,
             'mgmt_only': True,
             'description': 'New description',
             'description': 'New description',
+            'poe_mode': InterfacePoEModeChoices.MODE_PD,
+            'poe_type': InterfacePoETypeChoices.TYPE_2_8023AT,
             'mode': InterfaceModeChoices.MODE_TAGGED,
             'mode': InterfaceModeChoices.MODE_TAGGED,
             'tx_power': 10,
             'tx_power': 10,
             'untagged_vlan': vlans[0].pk,
             'untagged_vlan': vlans[0].pk,
@@ -2252,10 +2258,10 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            f"device,name,type,vrf.pk",
-            f"Device 1,Interface 4,1000base-t,{vrfs[0].pk}",
-            f"Device 1,Interface 5,1000base-t,{vrfs[0].pk}",
-            f"Device 1,Interface 6,1000base-t,{vrfs[0].pk}",
+            f"device,name,type,vrf.pk,poe_mode,poe_type",
+            f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
+            f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
+            f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
         )
         )
 
 
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])

+ 8 - 0
netbox/templates/dcim/interface.html

@@ -69,6 +69,14 @@
               <th scope="row">Description</th>
               <th scope="row">Description</th>
               <td>{{ object.description|placeholder }} </td>
               <td>{{ object.description|placeholder }} </td>
             </tr>
             </tr>
+            <tr>
+              <th scope="row">PoE Mode</th>
+              <td>{{ object.get_poe_mode_display|placeholder }}</td>
+            </tr>
+            <tr>
+              <th scope="row">PoE Mode</th>
+              <td>{{ object.get_poe_type_display|placeholder }}</td>
+            </tr>
             <tr>
             <tr>
               <th scope="row">802.1Q Mode</th>
               <th scope="row">802.1Q Mode</th>
               <td>{{ object.get_mode_display|placeholder }}</td>
               <td>{{ object.get_mode_display|placeholder }}</td>