From 67ffecc7099df7cdf4068f9bd1e14ff93663855b Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Thu, 19 Feb 2026 11:51:29 -0500 Subject: [PATCH 01/16] begin --- ...2026-02-17_16-20_add_interface_fec_mode.py | 26 +++++++++++++++++++ .../middlewared/api/v26_0_0/interface.py | 14 ++++++++++ .../plugins/interface/addresses.py | 10 +++++++ .../middlewared/plugins/network.py | 7 +++++ 4 files changed, 57 insertions(+) create mode 100644 src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py diff --git a/src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py b/src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py new file mode 100644 index 0000000000000..b79fb4d8c817a --- /dev/null +++ b/src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py @@ -0,0 +1,26 @@ +"""Add FEC mode to network interfaces. + +Revision ID: ad6bd79a37d7 +Revises: a8f5d9e2c1b7 +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 = 'a8f5d9e2c1b7' +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') diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index 78d57a97aa54b..f3d0dc9d01daa 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -117,6 +117,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): @@ -140,6 +142,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 @@ -291,6 +295,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 ------------------- # diff --git a/src/middlewared/middlewared/plugins/interface/addresses.py b/src/middlewared/middlewared/plugins/interface/addresses.py index 30e3aef819c29..97ab97e790e79 100644 --- a/src/middlewared/middlewared/plugins/interface/addresses.py +++ b/src/middlewared/middlewared/plugins/interface/addresses.py @@ -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 @@ -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: diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 9071f997cc199..7a6b1e69ac2a3 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -81,6 +81,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): @@ -254,6 +255,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) @@ -1088,6 +1092,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): @@ -1221,6 +1226,8 @@ 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 and new['type'] != 'PHYSICAL': + verrors.add('interface.update.fec_mode', 'FEC mode can only be set on physical interfaces.') verrors.check() From d16888374ed500ee562e9b625ac3ff3cd00f7206 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Thu, 19 Feb 2026 14:31:26 -0500 Subject: [PATCH 02/16] draft --- .../middlewared/api/v26_0_0/interface.py | 13 +++++++++- .../middlewared/plugins/network.py | 24 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index f3d0dc9d01daa..01038e96eb692 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -9,7 +9,8 @@ __all__ = [ - "InterfaceEntry", "InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult", + "InterfaceEntry", "InterfaceAvailableFecModesArgs", "InterfaceAvailableFecModesResult", + "InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult", "InterfaceCancelRollbackArgs", "InterfaceCancelRollbackResult", "InterfaceCheckinArgs", "InterfaceCheckinResult", "InterfaceCheckinWaitingArgs", "InterfaceCheckinWaitingResult", "InterfaceChoicesArgs", "InterfaceChoicesResult", "InterfaceCommitArgs", "InterfaceCommitResult", "InterfaceCreateArgs", "InterfaceCreateResult", @@ -535,3 +536,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[Literal["AUTO", "RS", "BASER", "OFF", "LLRS"]] + """List of FEC modes supported by the interface. Empty list if FEC is not supported.""" diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 7a6b1e69ac2a3..6f722891efb63 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -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, @@ -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 @@ -1227,7 +1229,11 @@ 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 and new['type'] != 'PHYSICAL': - verrors.add('interface.update.fec_mode', 'FEC mode can only be set on physical interfaces.') + verrors.add('interface_update.fec_mode', 'FEC mode can only be set on physical interfaces.') + elif 'fec_mode' in data: + available = await self.middleware.call('interface.available_fec_modes', oid) + if available and data['fec_mode'] not in available: + verrors.add('interface_update.fec_mode', f'Unsupported FEC mode. Available: {available}') verrors.check() @@ -1472,6 +1478,20 @@ 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 physical interface `id`. + + Returns an empty list for non-physical interfaces or interfaces without FEC support. + """ + iface = await self.get_instance(id_) + if iface['type'] != 'PHYSICAL': + return [] + return await self.middleware.run_in_thread( + lambda: get_ethtool().get_fec_modes(iface['name']) + ) + @api_method(InterfaceChoicesArgs, InterfaceChoicesResult, roles=['NETWORK_INTERFACE_READ']) async def choices(self, options): """ From 487333c56bf1e83a61bc804e41ddd100a67e1070 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Thu, 19 Feb 2026 14:52:48 -0500 Subject: [PATCH 03/16] touch up --- .../middlewared/api/v26_0_0/interface.py | 3 ++- .../middlewared/plugins/network.py | 22 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index 01038e96eb692..941251a494357 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -2,6 +2,7 @@ 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, @@ -544,5 +545,5 @@ class InterfaceAvailableFecModesArgs(BaseModel): class InterfaceAvailableFecModesResult(BaseModel): - result: list[Literal["AUTO", "RS", "BASER", "OFF", "LLRS"]] + result: list[FecModeName] """List of FEC modes supported by the interface. Empty list if FEC is not supported.""" diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 6f722891efb63..94f16624a784b 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -1228,12 +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 and new['type'] != 'PHYSICAL': - verrors.add('interface_update.fec_mode', 'FEC mode can only be set on physical interfaces.') - elif 'fec_mode' in data: - available = await self.middleware.call('interface.available_fec_modes', oid) - if available and data['fec_mode'] not in available: - verrors.add('interface_update.fec_mode', f'Unsupported FEC mode. Available: {available}') + 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() @@ -1488,9 +1494,7 @@ async def available_fec_modes(self, id_): iface = await self.get_instance(id_) if iface['type'] != 'PHYSICAL': return [] - return await self.middleware.run_in_thread( - lambda: get_ethtool().get_fec_modes(iface['name']) - ) + return await self.middleware.run_in_thread(get_ethtool().get_fec_modes, iface['name']) @api_method(InterfaceChoicesArgs, InterfaceChoicesResult, roles=['NETWORK_INTERFACE_READ']) async def choices(self, options): From cb2cfcbea07987d48f55a0c59a5e869b90e5c6d1 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Fri, 20 Feb 2026 10:00:37 -0500 Subject: [PATCH 04/16] remove unnecessary get_instance call --- src/middlewared/middlewared/plugins/network.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 94f16624a784b..36d7fb076033d 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -1487,14 +1487,11 @@ async def lacpdu_rate_choices(self): @api_method(InterfaceAvailableFecModesArgs, InterfaceAvailableFecModesResult, roles=['NETWORK_INTERFACE_READ']) async def available_fec_modes(self, id_): """ - Returns FEC modes supported by physical interface `id`. + Returns FEC modes supported by interface `id`. - Returns an empty list for non-physical interfaces or interfaces without FEC support. + Returns an empty list if the interface does not exist or does not support FEC. """ - iface = await self.get_instance(id_) - if iface['type'] != 'PHYSICAL': - return [] - return await self.middleware.run_in_thread(get_ethtool().get_fec_modes, iface['name']) + 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): From 0cff75554c5e7967e5e462f70d135175f88875f3 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Wed, 11 Mar 2026 14:35:07 -0400 Subject: [PATCH 05/16] 27 api --- .../middlewared/api/v27_0_0/interface.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/middlewared/middlewared/api/v27_0_0/interface.py b/src/middlewared/middlewared/api/v27_0_0/interface.py index 78d57a97aa54b..e0aa5fa49cb5b 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_0_0/interface.py @@ -2,6 +2,7 @@ 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, @@ -9,7 +10,8 @@ __all__ = [ - "InterfaceEntry", "InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult", + "InterfaceEntry", "InterfaceAvailableFecModesArgs", "InterfaceAvailableFecModesResult", + "InterfaceBridgeMembersChoicesArgs", "InterfaceBridgeMembersChoicesResult", "InterfaceCancelRollbackArgs", "InterfaceCancelRollbackResult", "InterfaceCheckinArgs", "InterfaceCheckinResult", "InterfaceCheckinWaitingArgs", "InterfaceCheckinWaitingResult", "InterfaceChoicesArgs", "InterfaceChoicesResult", "InterfaceCommitArgs", "InterfaceCommitResult", "InterfaceCreateArgs", "InterfaceCreateResult", @@ -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): @@ -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 @@ -291,6 +297,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 ------------------- # @@ -521,3 +536,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.""" From b91c4e4ccd54cd2370a44b57933eeb29d9a226a3 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Wed, 11 Mar 2026 14:46:23 -0400 Subject: [PATCH 06/16] update migration --- ...fec_mode.py => 2026-03-11_18-44_add_interface_fec_mode.py} | 4 ++-- .../alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/middlewared/middlewared/alembic/versions/26.0/{2026-02-17_16-20_add_interface_fec_mode.py => 2026-03-11_18-44_add_interface_fec_mode.py} (91%) diff --git a/src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py b/src/middlewared/middlewared/alembic/versions/26.0/2026-03-11_18-44_add_interface_fec_mode.py similarity index 91% rename from src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py rename to src/middlewared/middlewared/alembic/versions/26.0/2026-03-11_18-44_add_interface_fec_mode.py index b79fb4d8c817a..0e39b2337649a 100644 --- a/src/middlewared/middlewared/alembic/versions/26.0/2026-02-17_16-20_add_interface_fec_mode.py +++ b/src/middlewared/middlewared/alembic/versions/26.0/2026-03-11_18-44_add_interface_fec_mode.py @@ -1,7 +1,7 @@ """Add FEC mode to network interfaces. Revision ID: ad6bd79a37d7 -Revises: a8f5d9e2c1b7 +Revises: f3b4b0f4b0cf Create Date: 2026-02-17 16:20:00.000000+00:00 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'ad6bd79a37d7' -down_revision = 'a8f5d9e2c1b7' +down_revision = 'f3b4b0f4b0cf' branch_labels = None depends_on = None diff --git a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py index af5f48ea5fed8..eddd9c5310d93 100644 --- a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py @@ -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 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = '3fc58b28ce77' -down_revision = 'f3b4b0f4b0cf' +down_revision = 'ad6bd79a37d7' branch_labels = None depends_on = None From eb7a4a66615daf42f8fcebe9a4dfade09c0bedca Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Wed, 11 Mar 2026 16:00:53 -0400 Subject: [PATCH 07/16] clarify state field --- src/middlewared/middlewared/api/v27_0_0/interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/middlewared/middlewared/api/v27_0_0/interface.py b/src/middlewared/middlewared/api/v27_0_0/interface.py index e0aa5fa49cb5b..5773cc9f6993f 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_0_0/interface.py @@ -133,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 From 9d9fd76b67918e3e8bb41ff698d92b8cdd47100a Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Fri, 13 Mar 2026 13:41:35 -0400 Subject: [PATCH 08/16] merge migration --- .../27.0/2026-03-09_19-16_ScrubNotStarted.py | 4 ++-- .../versions/27.0/2026-03-13_17-05_.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py diff --git a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py index eddd9c5310d93..af5f48ea5fed8 100644 --- a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-09_19-16_ScrubNotStarted.py @@ -1,7 +1,7 @@ """Remove ScrubNotStarted alerts Revision ID: 3fc58b28ce77 -Revises: ad6bd79a37d7 +Revises: f3b4b0f4b0cf Create Date: 2026-03-09 19:16:31.067201+00:00 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = '3fc58b28ce77' -down_revision = 'ad6bd79a37d7' +down_revision = 'f3b4b0f4b0cf' branch_labels = None depends_on = None diff --git a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py new file mode 100644 index 0000000000000..03dd0a275cb26 --- /dev/null +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py @@ -0,0 +1,24 @@ +"""empty message + +Revision ID: ef6c293fc34f +Revises: 3fc58b28ce77 +Create Date: 2026-03-13 17:05:46.399951+00:00 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ef6c293fc34f' +down_revision = ('ad6bd79a37d7', '3fc58b28ce77') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass From 2e0373f39a67ca2056aea221ccb5865ef8abd714 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Fri, 13 Mar 2026 14:03:14 -0400 Subject: [PATCH 09/16] make available_fec_modes synchronous --- src/middlewared/middlewared/plugins/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 36d7fb076033d..6379b228c8647 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -1230,7 +1230,7 @@ async def do_update(self, audit_callback, oid, data): 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 available := self.available_fec_modes(oid): if data['fec_mode'] not in available: verrors.add( 'interface_update.fec_mode', @@ -1485,13 +1485,13 @@ 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_): + 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_) + return get_ethtool().get_fec_modes(id_) @api_method(InterfaceChoicesArgs, InterfaceChoicesResult, roles=['NETWORK_INTERFACE_READ']) async def choices(self, options): From 108b78391b9f0cdfe1f38d0b73225f44b6c900b6 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Fri, 13 Mar 2026 14:14:05 -0400 Subject: [PATCH 10/16] rename migration --- .../27.0/{2026-03-13_17-05_.py => 2026-03-13_17-05_merge.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/middlewared/middlewared/alembic/versions/27.0/{2026-03-13_17-05_.py => 2026-03-13_17-05_merge.py} (95%) diff --git a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_merge.py similarity index 95% rename from src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py rename to src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_merge.py index 03dd0a275cb26..fe566cc03a55a 100644 --- a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_.py +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_merge.py @@ -1,4 +1,4 @@ -"""empty message +"""Merge. Revision ID: ef6c293fc34f Revises: 3fc58b28ce77 From bccdbfe8d795d3b4c40e6d1ddb408ecc0b40c1ad Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Tue, 17 Mar 2026 10:28:16 -0400 Subject: [PATCH 11/16] only allow changing fec_mode on enterprise systems --- .../middlewared/plugins/network.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 6379b228c8647..9aa5a4fbd0e0b 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -1228,18 +1228,25 @@ 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.') + + # Validate fec_mode field if 'fec_mode' in data: - if new['type'] == 'PHYSICAL': - if available := self.available_fec_modes(oid): - if data['fec_mode'] not in available: + if await self.middleware.call('system.is_enterprise'): + if new['type'] == 'PHYSICAL': + if available := 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', - f'Unsupported FEC mode. Available: {", ".join(available)}' + 'interface_update.fec_mode', 'This interface does not support FEC mode configuration.' ) else: - verrors.add('interface_update.fec_mode', 'This interface does not support FEC mode configuration.') + verrors.add('interface_update.fec_mode', 'FEC mode can only be set on physical interfaces.') else: - verrors.add('interface_update.fec_mode', 'FEC mode can only be set on physical interfaces.') + verrors.add('interface_update.fec_mode', 'Configuring FEC mode is an enterprise feature.') verrors.check() From 4c9604dffd4c9b4e255d694dd38fef7ee00af190 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Tue, 17 Mar 2026 10:37:59 -0400 Subject: [PATCH 12/16] update merge migration --- .../{2026-03-13_17-05_merge.py => 2026-03-17_14-37_merge.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/middlewared/middlewared/alembic/versions/27.0/{2026-03-13_17-05_merge.py => 2026-03-17_14-37_merge.py} (80%) diff --git a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_merge.py b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py similarity index 80% rename from src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_merge.py rename to src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py index fe566cc03a55a..6a99294bf9604 100644 --- a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-13_17-05_merge.py +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py @@ -1,7 +1,7 @@ """Merge. Revision ID: ef6c293fc34f -Revises: 3fc58b28ce77 +Revises: aa197df8684c Create Date: 2026-03-13 17:05:46.399951+00:00 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'ef6c293fc34f' -down_revision = ('ad6bd79a37d7', '3fc58b28ce77') +down_revision = ('ad6bd79a37d7', 'aa197df8684c') branch_labels = None depends_on = None From 11ca975eb97aa31fe846fea8312712403a638c31 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Tue, 17 Mar 2026 18:19:42 -0400 Subject: [PATCH 13/16] API doc --- src/middlewared/middlewared/api/v26_0_0/interface.py | 4 ++++ src/middlewared/middlewared/api/v27_0_0/interface.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index 941251a494357..0584ee90505aa 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -301,6 +301,10 @@ class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass): """ Forward Error Correction (FEC) mode. Only valid for physical interfaces. + Configuring this field is only available on enterprise systems. It should be used as directed by TrueNAS Support \ + for resolving link negotiation failures due to FEC mismatches with the upstream switch. Improper configuration of \ + this field can cause a healthy interface to go down. + * "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) diff --git a/src/middlewared/middlewared/api/v27_0_0/interface.py b/src/middlewared/middlewared/api/v27_0_0/interface.py index 5773cc9f6993f..00467f7e1db34 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_0_0/interface.py @@ -303,6 +303,11 @@ class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass): fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] """ Forward Error Correction (FEC) mode. Only valid for physical interfaces. + + Configuring this field is only available on enterprise systems. It should be used as directed by TrueNAS Support \ + for resolving link negotiation failures due to FEC mismatches with the upstream switch. Improper configuration of \ + this field can cause a healthy interface to go down. + * "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) From 96c3c11c43f88125042e1cf08ad298ea010466aa Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Tue, 17 Mar 2026 18:25:10 -0400 Subject: [PATCH 14/16] appease import linter --- src/middlewared/middlewared/api/v26_0_0/interface.py | 4 +++- src/middlewared/middlewared/api/v27_0_0/interface.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index 0584ee90505aa..325cc8be5e9ed 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -2,7 +2,6 @@ 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, @@ -28,6 +27,9 @@ ] +FecModeName = Literal["AUTO", "OFF", "RS", "BASER", "LLRS"] + + class InterfaceEntryAlias(BaseModel): type: str """The type of IP address (INET for IPv4, INET6 for IPv6).""" diff --git a/src/middlewared/middlewared/api/v27_0_0/interface.py b/src/middlewared/middlewared/api/v27_0_0/interface.py index 00467f7e1db34..9b93f723124db 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_0_0/interface.py @@ -2,7 +2,6 @@ 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, @@ -28,6 +27,9 @@ ] +FecModeName = Literal["AUTO", "OFF", "RS", "BASER", "LLRS"] + + class InterfaceEntryAlias(BaseModel): type: str """The type of IP address (INET for IPv4, INET6 for IPv6).""" From 07cb0b0d8d580fc437ebdc35d743730b1f1e5ee4 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Wed, 18 Mar 2026 14:06:53 -0400 Subject: [PATCH 15/16] use FecModeName alias consistently --- src/middlewared/middlewared/api/v26_0_0/interface.py | 6 +++--- src/middlewared/middlewared/api/v27_0_0/interface.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index 325cc8be5e9ed..acf0749fbebfa 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -121,7 +121,7 @@ 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 + fec_mode: FecModeName | None = NotRequired """Currently active Forward Error Correction mode from the hardware. Only present for physical interfaces.""" @@ -146,7 +146,7 @@ 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 + fec_mode: FecModeName = NotRequired """Forward Error Correction (FEC) mode. Only valid for physical interfaces.""" vlan_parent_interface: str | None = NotRequired """Parent interface for VLAN configuration.""" @@ -299,7 +299,7 @@ class InterfaceServicesRestartedOnSyncItem(BaseModel): class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass): type: Excluded = excluded_field() - fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] + fec_mode: FecModeName """ Forward Error Correction (FEC) mode. Only valid for physical interfaces. diff --git a/src/middlewared/middlewared/api/v27_0_0/interface.py b/src/middlewared/middlewared/api/v27_0_0/interface.py index 9b93f723124db..cfe170da62193 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_0_0/interface.py @@ -121,7 +121,7 @@ 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 + fec_mode: FecModeName | None = NotRequired """Currently active Forward Error Correction mode from the hardware. Only present for physical interfaces.""" @@ -149,7 +149,7 @@ 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 + fec_mode: FecModeName = NotRequired """Forward Error Correction (FEC) mode. Only valid for physical interfaces.""" vlan_parent_interface: str | None = NotRequired """Parent interface for VLAN configuration.""" @@ -302,7 +302,7 @@ class InterfaceServicesRestartedOnSyncItem(BaseModel): class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass): type: Excluded = excluded_field() - fec_mode: Literal["AUTO", "RS", "BASER", "OFF", "LLRS"] + fec_mode: FecModeName """ Forward Error Correction (FEC) mode. Only valid for physical interfaces. From a1c12dc9fdd41d4e35d145a8063ec9b12a71b072 Mon Sep 17 00:00:00 2001 From: Logan Cary Date: Wed, 18 Mar 2026 14:32:49 -0400 Subject: [PATCH 16/16] address reviews --- .../versions/27.0/2026-03-17_14-37_merge.py | 6 ++-- .../middlewared/api/v26_0_0/interface.py | 28 +++++++++++++++++-- .../middlewared/api/v27_0_0/interface.py | 28 +++++++++++++++++-- .../middlewared/plugins/network.py | 8 ++++-- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py index 6a99294bf9604..dc4a154e650b6 100644 --- a/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py @@ -1,7 +1,7 @@ -"""Merge. +"""Merge migration for NAS-139477 adding FEC mode (revision ad6bd79a37d7). Revision ID: ef6c293fc34f -Revises: aa197df8684c +Revises: aa197df8684c, ad6bd79a37d7 Create Date: 2026-03-13 17:05:46.399951+00:00 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'ef6c293fc34f' -down_revision = ('ad6bd79a37d7', 'aa197df8684c') +down_revision = ('aa197df8684c', 'ad6bd79a37d7') branch_labels = None depends_on = None diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index acf0749fbebfa..8a636ab6d051f 100644 --- a/src/middlewared/middlewared/api/v26_0_0/interface.py +++ b/src/middlewared/middlewared/api/v26_0_0/interface.py @@ -122,7 +122,15 @@ class InterfaceEntryState(BaseModel): """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: FecModeName | None = NotRequired - """Currently active Forward Error Correction mode from the hardware. Only present for physical interfaces.""" + """Currently active Forward Error Correction mode from the hardware. Only present 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 + * `null`: FEC not supported or mode cannot be determined + """ class InterfaceEntry(BaseModel): @@ -147,7 +155,14 @@ class InterfaceEntry(BaseModel): mtu: int | None """Maximum transmission unit size for the interface.""" fec_mode: FecModeName = NotRequired - """Forward Error Correction (FEC) mode. Only valid for physical interfaces.""" + """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 + """ vlan_parent_interface: str | None = NotRequired """Parent interface for VLAN configuration.""" vlan_tag: int | None = NotRequired @@ -552,4 +567,11 @@ class InterfaceAvailableFecModesArgs(BaseModel): class InterfaceAvailableFecModesResult(BaseModel): result: list[FecModeName] - """List of FEC modes supported by the interface. Empty list if FEC is not supported.""" + """List of FEC modes supported by the interface. Empty list if FEC is not supported. + + * "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 + """ diff --git a/src/middlewared/middlewared/api/v27_0_0/interface.py b/src/middlewared/middlewared/api/v27_0_0/interface.py index cfe170da62193..d9c3e5f59aef6 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_0_0/interface.py @@ -122,7 +122,15 @@ class InterfaceEntryState(BaseModel): """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: FecModeName | None = NotRequired - """Currently active Forward Error Correction mode from the hardware. Only present for physical interfaces.""" + """Currently active Forward Error Correction mode from the hardware. Only present 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 + * `null`: FEC not supported or mode cannot be determined + """ class InterfaceEntry(BaseModel): @@ -150,7 +158,14 @@ class InterfaceEntry(BaseModel): mtu: int | None """Maximum transmission unit size for the interface.""" fec_mode: FecModeName = NotRequired - """Forward Error Correction (FEC) mode. Only valid for physical interfaces.""" + """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 + """ vlan_parent_interface: str | None = NotRequired """Parent interface for VLAN configuration.""" vlan_tag: int | None = NotRequired @@ -555,4 +570,11 @@ class InterfaceAvailableFecModesArgs(BaseModel): class InterfaceAvailableFecModesResult(BaseModel): result: list[FecModeName] - """List of FEC modes supported by the interface. Empty list if FEC is not supported.""" + """List of FEC modes supported by the interface. Empty list if FEC is not supported. + + * "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 + """ diff --git a/src/middlewared/middlewared/plugins/network.py b/src/middlewared/middlewared/plugins/network.py index 9aa5a4fbd0e0b..6dfdaad82d0c1 100644 --- a/src/middlewared/middlewared/plugins/network.py +++ b/src/middlewared/middlewared/plugins/network.py @@ -33,7 +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.ethtool import NetlinkError, 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 @@ -1498,7 +1498,11 @@ def available_fec_modes(self, id_): Returns an empty list if the interface does not exist or does not support FEC. """ - return get_ethtool().get_fec_modes(id_) + try: + return get_ethtool().get_fec_modes(id_) + except NetlinkError as e: + self.logger.error(f'Failed to retrieve available FEC modes for {id_}', exc_info=e) + return [] @api_method(InterfaceChoicesArgs, InterfaceChoicesResult, roles=['NETWORK_INTERFACE_READ']) async def choices(self, options):