Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Add FEC mode to network interfaces.
Revision ID: ad6bd79a37d7
Revises: f3b4b0f4b0cf
Create Date: 2026-02-17 16:20:00.000000+00:00
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'ad6bd79a37d7'
down_revision = 'f3b4b0f4b0cf'
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table('network_interfaces', schema=None) as batch_op:
batch_op.add_column(sa.Column('int_fec_mode', sa.String(length=10), nullable=True))


def downgrade():
with op.batch_alter_table('network_interfaces', schema=None) as batch_op:
batch_op.drop_column('int_fec_mode')
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Remove ScrubNotStarted alerts

Revision ID: 3fc58b28ce77
Revises: f3b4b0f4b0cf
Revises: ad6bd79a37d7
Create Date: 2026-03-09 19:16:31.067201+00:00

"""
Expand All @@ -11,7 +11,7 @@

# revision identifiers, used by Alembic.
revision = '3fc58b28ce77'
down_revision = 'f3b4b0f4b0cf'
down_revision = 'ad6bd79a37d7'
branch_labels = None
depends_on = None

Expand Down
28 changes: 27 additions & 1 deletion src/middlewared/middlewared/api/v26_0_0/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

from pydantic import Field

from truenas_pynetif.ethtool import FecModeName
from middlewared.api.base import (
BaseModel, IPv4Address, UniqueList, IPvAnyAddress, Excluded, excluded_field, ForUpdateMetaclass,
single_argument_args, single_argument_result, NotRequired, NonEmptyString,
)


__all__ = [
"InterfaceEntry", "InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult",
"InterfaceEntry", "InterfaceAvailableFecModesArgs", "InterfaceAvailableFecModesResult",
"InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult",
"InterfaceCancelRollbackArgs", "InterfaceCancelRollbackResult", "InterfaceCheckinArgs", "InterfaceCheckinResult",
"InterfaceCheckinWaitingArgs", "InterfaceCheckinWaitingResult", "InterfaceChoicesArgs", "InterfaceChoicesResult",
"InterfaceCommitArgs", "InterfaceCommitResult", "InterfaceCreateArgs", "InterfaceCreateResult",
Expand Down Expand Up @@ -117,6 +119,8 @@ class InterfaceEntryState(BaseModel):
pcp: int | None = NotRequired
"""Priority Code Point for VLAN traffic prioritization. Values 0-7 map to different QoS priority levels, \
with 0 being lowest and 7 highest priority."""
fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] | None = NotRequired
"""Currently active Forward Error Correction mode from the hardware. Only present for physical interfaces."""


class InterfaceEntry(BaseModel):
Expand All @@ -140,6 +144,8 @@ class InterfaceEntry(BaseModel):
"""Human-readable description of the interface."""
mtu: int | None
"""Maximum transmission unit size for the interface."""
fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] = NotRequired
"""Forward Error Correction (FEC) mode. Only valid for physical interfaces."""
vlan_parent_interface: str | None = NotRequired
"""Parent interface for VLAN configuration."""
vlan_tag: int | None = NotRequired
Expand Down Expand Up @@ -291,6 +297,16 @@ class InterfaceServicesRestartedOnSyncItem(BaseModel):

class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass):
type: Excluded = excluded_field()
fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"]
"""
Forward Error Correction (FEC) mode. Only valid for physical interfaces.

* "AUTO": Selects the best FEC mode based on cable/port capabilities
* "RS": RS-FEC (Reed-Solomon), often used for 25GbE/100GbE+ NICs
* "BASER": BaseR-FEC (FireCode)
* "OFF": Disables FEC
* "LLRS": Low Latency Reed-Solomon FEC, used for 25GBASE-KR/CR
"""


# ------------------- Args and Results ------------------- #
Expand Down Expand Up @@ -521,3 +537,13 @@ class InterfaceXmitHashPolicyChoicesResult(BaseModel):
"""Use MAC and IP addresses for traffic distribution across bond members."""
LAYER3_4: Literal["LAYER3+4"] = Field(alias="LAYER3+4")
"""Use MAC, IP, and TCP/UDP port information for traffic distribution across bond members."""


class InterfaceAvailableFecModesArgs(BaseModel):
id: str
"""ID of the interface to query for supported FEC modes."""


class InterfaceAvailableFecModesResult(BaseModel):
result: list[FecModeName]
"""List of FEC modes supported by the interface. Empty list if FEC is not supported."""
32 changes: 30 additions & 2 deletions src/middlewared/middlewared/api/v27_0_0/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

from pydantic import Field

from truenas_pynetif.ethtool import FecModeName
from middlewared.api.base import (
BaseModel, IPv4Address, UniqueList, IPvAnyAddress, Excluded, excluded_field, ForUpdateMetaclass,
single_argument_args, single_argument_result, NotRequired, NonEmptyString,
)


__all__ = [
"InterfaceEntry", "InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult",
"InterfaceEntry", "InterfaceAvailableFecModesArgs", "InterfaceAvailableFecModesResult",
"InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult",
"InterfaceCancelRollbackArgs", "InterfaceCancelRollbackResult", "InterfaceCheckinArgs", "InterfaceCheckinResult",
"InterfaceCheckinWaitingArgs", "InterfaceCheckinWaitingResult", "InterfaceChoicesArgs", "InterfaceChoicesResult",
"InterfaceCommitArgs", "InterfaceCommitResult", "InterfaceCreateArgs", "InterfaceCreateResult",
Expand Down Expand Up @@ -117,6 +119,8 @@ class InterfaceEntryState(BaseModel):
pcp: int | None = NotRequired
"""Priority Code Point for VLAN traffic prioritization. Values 0-7 map to different QoS priority levels, \
with 0 being lowest and 7 highest priority."""
fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] | None = NotRequired
"""Currently active Forward Error Correction mode from the hardware. Only present for physical interfaces."""


class InterfaceEntry(BaseModel):
Expand All @@ -129,7 +133,10 @@ class InterfaceEntry(BaseModel):
type: str
"""Type of interface (PHYSICAL, BRIDGE, LINK_AGGREGATION, VLAN, etc.)."""
state: InterfaceEntryState
"""Current runtime state information for the interface."""
"""Current runtime state of the interface as reported by the OS kernel. This reflects what is actually configured \
in the kernel at query time and may differ from the top-level fields, which represent the persisted database \
configuration. Pending changes (after `interface.update` but before `interface.commit`) will not be visible here \
until the commit is applied and the interface is synchronized."""
aliases: list[InterfaceEntryAlias]
"""List of IP address aliases configured on the interface."""
ipv4_dhcp: bool
Expand All @@ -140,6 +147,8 @@ class InterfaceEntry(BaseModel):
"""Human-readable description of the interface."""
mtu: int | None
"""Maximum transmission unit size for the interface."""
fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] = NotRequired
"""Forward Error Correction (FEC) mode. Only valid for physical interfaces."""
vlan_parent_interface: str | None = NotRequired
"""Parent interface for VLAN configuration."""
vlan_tag: int | None = NotRequired
Expand Down Expand Up @@ -291,6 +300,15 @@ class InterfaceServicesRestartedOnSyncItem(BaseModel):

class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass):
type: Excluded = excluded_field()
fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"]
"""
Forward Error Correction (FEC) mode. Only valid for physical interfaces.
* "AUTO": Selects the best FEC mode based on cable/port capabilities
* "RS": RS-FEC (Reed-Solomon), often used for 25GbE/100GbE+ NICs
* "BASER": BaseR-FEC (FireCode)
* "OFF": Disables FEC
* "LLRS": Low Latency Reed-Solomon FEC, used for 25GBASE-KR/CR
"""


# ------------------- Args and Results ------------------- #
Expand Down Expand Up @@ -521,3 +539,13 @@ class InterfaceXmitHashPolicyChoicesResult(BaseModel):
"""Use MAC and IP addresses for traffic distribution across bond members."""
LAYER3_4: Literal["LAYER3+4"] = Field(alias="LAYER3+4")
"""Use MAC, IP, and TCP/UDP port information for traffic distribution across bond members."""


class InterfaceAvailableFecModesArgs(BaseModel):
id: str
"""ID of the interface to query for supported FEC modes."""


class InterfaceAvailableFecModesResult(BaseModel):
result: list[FecModeName]
"""List of FEC modes supported by the interface. Empty list if FEC is not supported."""
10 changes: 10 additions & 0 deletions src/middlewared/middlewared/plugins/interface/addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from truenas_pynetif.address.constants import AddressFamily, IFFlags
from truenas_pynetif.address.get_ipaddresses import get_link_addresses
from truenas_pynetif.address.link import set_link_alias, set_link_mtu, set_link_up
from truenas_pynetif.ethtool import DeviceNotFound, get_ethtool, OperationNotSupported
from truenas_pynetif.netlink import AddressDoesNotExist, AddressInfo, LinkInfo

from middlewared.plugins.interface.dhcp import dhcp_leases, dhcp_status, dhcp_stop
Expand Down Expand Up @@ -204,6 +205,15 @@ def configure_addresses_impl(
elif link.mtu != 1500:
set_link_mtu(sock, 1500, index=link_index)

# Apply FEC mode (physical interfaces only; virtual interfaces never have int_fec_mode set)
if fec_mode := data.get("int_fec_mode"):
try:
get_ethtool().set_fec(name, fec_mode)
except (OperationNotSupported, DeviceNotFound):
pass
except Exception:
ctx.logger.warning("Failed to set FEC mode on %s", name, exc_info=True)

# Set interface description
if data["int_name"]:
try:
Expand Down
30 changes: 29 additions & 1 deletion src/middlewared/middlewared/plugins/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from middlewared.api import api_method
from middlewared.plugins.interface.dhcp import dhcp_start
from middlewared.api.current import (
InterfaceEntry, InterfaceBridgeMembersChoicesArgs, InterfaceBridgeMembersChoicesResult,
InterfaceEntry, InterfaceAvailableFecModesArgs, InterfaceAvailableFecModesResult,
InterfaceBridgeMembersChoicesArgs, InterfaceBridgeMembersChoicesResult,
InterfaceCancelRollbackArgs, InterfaceCancelRollbackResult, InterfaceCheckinArgs, InterfaceCheckinResult,
InterfaceCheckinWaitingArgs, InterfaceCheckinWaitingResult, InterfaceChoicesArgs, InterfaceChoicesResult,
InterfaceCommitArgs, InterfaceCommitResult, InterfaceCreateArgs, InterfaceCreateResult,
Expand All @@ -32,6 +33,7 @@
from middlewared.utils.filter_list import filter_list
from truenas_pynetif.address.constants import AddressFamily
from truenas_pynetif.address.netlink import get_addresses, get_default_route, netlink_route
from truenas_pynetif.ethtool import get_ethtool
from truenas_pynetif.interface import CLONED_PREFIXES
from truenas_pynetif.interface_state import list_interface_states
from truenas_pynetif.utils import INTERNAL_INTERFACES
Expand Down Expand Up @@ -81,6 +83,7 @@ class NetworkInterfaceModel(sa.Model):
int_critical = sa.Column(sa.Boolean(), default=False)
int_group = sa.Column(sa.Integer(), nullable=True)
int_mtu = sa.Column(sa.Integer(), nullable=True)
int_fec_mode = sa.Column(sa.String(10), nullable=True)


class NetworkInterfaceLinkAddressModel(sa.Model):
Expand Down Expand Up @@ -254,6 +257,9 @@ def iface_extend(self, iface_state, configs, ha_hardware, fake=False):
'description': config['int_name'],
'mtu': config['int_mtu'],
})
if itype == InterfaceType.PHYSICAL:
if fec_mode := config.get('int_fec_mode'):
iface['fec_mode'] = fec_mode

if ha_hardware:
info = ('INET', 32) if config['int_version'] == 4 else ('INET6', 128)
Expand Down Expand Up @@ -1088,6 +1094,7 @@ async def __convert_interface_datastore(self, data):
'critical': data.get('failover_critical') or False,
'group': data.get('failover_group'),
'mtu': data.get('mtu') or None,
'fec_mode': data.get('fec_mode'),
}

async def __create_interface_datastore(self, data, attrs):
Expand Down Expand Up @@ -1221,6 +1228,18 @@ async def do_update(self, audit_callback, oid, data):
)
if await self.middleware.call('failover.licensed') and (new.get('ipv4_dhcp') or new.get('ipv6_auto')):
verrors.add('interface_update.dhcp', 'Enabling DHCPv4/v6 on HA systems is unsupported.')
if 'fec_mode' in data:
if new['type'] == 'PHYSICAL':
if available := await self.available_fec_modes(oid):
if data['fec_mode'] not in available:
verrors.add(
'interface_update.fec_mode',
f'Unsupported FEC mode. Available: {", ".join(available)}'
)
else:
verrors.add('interface_update.fec_mode', 'This interface does not support FEC mode configuration.')
else:
verrors.add('interface_update.fec_mode', 'FEC mode can only be set on physical interfaces.')

verrors.check()

Expand Down Expand Up @@ -1465,6 +1484,15 @@ async def lacpdu_rate_choices(self):
"""
return {i.value: i.value for i in LacpduRateChoices}

@api_method(InterfaceAvailableFecModesArgs, InterfaceAvailableFecModesResult, roles=['NETWORK_INTERFACE_READ'])
async def available_fec_modes(self, id_):
"""
Returns FEC modes supported by interface `id`.

Returns an empty list if the interface does not exist or does not support FEC.
"""
return await self.middleware.run_in_thread(get_ethtool().get_fec_modes, id_)

@api_method(InterfaceChoicesArgs, InterfaceChoicesResult, roles=['NETWORK_INTERFACE_READ'])
async def choices(self, options):
"""
Expand Down
Loading