diff --git a/src/middlewared/middlewared/alembic/versions/26.0/2026-03-11_18-44_add_interface_fec_mode.py b/src/middlewared/middlewared/alembic/versions/26.0/2026-03-11_18-44_add_interface_fec_mode.py new file mode 100644 index 0000000000000..0e39b2337649a --- /dev/null +++ b/src/middlewared/middlewared/alembic/versions/26.0/2026-03-11_18-44_add_interface_fec_mode.py @@ -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') 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 new file mode 100644 index 0000000000000..dc4a154e650b6 --- /dev/null +++ b/src/middlewared/middlewared/alembic/versions/27.0/2026-03-17_14-37_merge.py @@ -0,0 +1,24 @@ +"""Merge migration for NAS-139477 adding FEC mode (revision ad6bd79a37d7). + +Revision ID: ef6c293fc34f +Revises: aa197df8684c, ad6bd79a37d7 +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 = ('aa197df8684c', 'ad6bd79a37d7') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/src/middlewared/middlewared/api/v26_0_0/interface.py b/src/middlewared/middlewared/api/v26_0_0/interface.py index 78d57a97aa54b..8a636ab6d051f 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", @@ -26,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).""" @@ -117,6 +121,16 @@ 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: FecModeName | None = NotRequired + """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): @@ -140,6 +154,15 @@ class InterfaceEntry(BaseModel): """Human-readable description of the interface.""" mtu: int | None """Maximum transmission unit size for the interface.""" + fec_mode: FecModeName = NotRequired + """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 @@ -291,6 +314,20 @@ class InterfaceServicesRestartedOnSyncItem(BaseModel): class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass): type: Excluded = excluded_field() + fec_mode: FecModeName + """ + 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) + * "OFF": Disables FEC + * "LLRS": Low Latency Reed-Solomon FEC, used for 25GBASE-KR/CR + """ # ------------------- Args and Results ------------------- # @@ -521,3 +558,20 @@ 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. + + * "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 78d57a97aa54b..d9c3e5f59aef6 100644 --- a/src/middlewared/middlewared/api/v27_0_0/interface.py +++ b/src/middlewared/middlewared/api/v27_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", @@ -26,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).""" @@ -117,6 +121,16 @@ 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: FecModeName | None = NotRequired + """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): @@ -129,7 +143,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 @@ -140,6 +157,15 @@ class InterfaceEntry(BaseModel): """Human-readable description of the interface.""" mtu: int | None """Maximum transmission unit size for the interface.""" + fec_mode: FecModeName = NotRequired + """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 @@ -291,6 +317,20 @@ class InterfaceServicesRestartedOnSyncItem(BaseModel): class InterfaceUpdate(InterfaceCreate, metaclass=ForUpdateMetaclass): type: Excluded = excluded_field() + fec_mode: FecModeName + """ + 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) + * "OFF": Disables FEC + * "LLRS": Low Latency Reed-Solomon FEC, used for 25GBASE-KR/CR + """ # ------------------- Args and Results ------------------- # @@ -521,3 +561,20 @@ 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. + + * "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/interface/addresses.py b/src/middlewared/middlewared/plugins/interface/addresses.py index 9a5931c8fba40..9ab9a0916dc2a 100644 --- a/src/middlewared/middlewared/plugins/interface/addresses.py +++ b/src/middlewared/middlewared/plugins/interface/addresses.py @@ -8,6 +8,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 @@ -216,6 +217,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..6dfdaad82d0c1 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 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 @@ -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): @@ -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) @@ -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): @@ -1222,6 +1229,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 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', 'This interface does not support FEC mode configuration.' + ) + else: + verrors.add('interface_update.fec_mode', 'FEC mode can only be set on physical interfaces.') + else: + verrors.add('interface_update.fec_mode', 'Configuring FEC mode is an enterprise feature.') + verrors.check() await self.__save_datastores() @@ -1465,6 +1491,19 @@ async def lacpdu_rate_choices(self): """ return {i.value: i.value for i in LacpduRateChoices} + @api_method(InterfaceAvailableFecModesArgs, InterfaceAvailableFecModesResult, roles=['NETWORK_INTERFACE_READ']) + 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. + """ + 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): """