diff --git a/backend/src/utilities/bridge_link_state_manager.py b/backend/src/utilities/bridge_link_state_manager.py index 38a902a..98cc101 100644 --- a/backend/src/utilities/bridge_link_state_manager.py +++ b/backend/src/utilities/bridge_link_state_manager.py @@ -552,58 +552,87 @@ class BridgeLinkStateWatcher: if source_profile is None or target_profile is None or source_profile == target_profile: return None - cmd = [_ETHTOOL_BIN, "-s", target_ifname] change_summary: Optional[str] = None + commands: list[tuple[list[str], str]] = [] if source_profile.autoneg is True: if source_profile.speed_mbps is not None and source_profile.duplex is not None: # A remote peer can pull this port down to a lower negotiated speed while autoneg # stays enabled locally. Mirror that effective mode while keeping autoneg enabled # on the sibling so its far-end peer can still negotiate successfully. - cmd.extend( + commands.append( + ( + [ + _ETHTOOL_BIN, + "-s", + target_ifname, + "speed", + str(source_profile.speed_mbps), + "duplex", + source_profile.duplex, + "autoneg", + "on", + ], + ( + f"{target_ifname} link={source_profile.speed_mbps}Mb/" + f"{source_profile.duplex}/autoneg-on" + ), + ) + ) + else: + commands.append(([_ETHTOOL_BIN, "-s", target_ifname, "autoneg", "on"], f"{target_ifname} link=autoneg-on")) + elif source_profile.autoneg is False and source_profile.speed_mbps is not None and source_profile.duplex is not None: + # Do not force autoneg off on sibling ports. The far-end peer on that segment may + # still rely on autoneg, and forcing a fixed mode here can leave the link down. + commands.append( + ( [ + _ETHTOOL_BIN, + "-s", + target_ifname, "speed", str(source_profile.speed_mbps), "duplex", source_profile.duplex, "autoneg", "on", - ] + ], + ( + f"{target_ifname} link={source_profile.speed_mbps}Mb/" + f"{source_profile.duplex}/autoneg-on-safe" + ), ) - change_summary = ( - f"{target_ifname} link={source_profile.speed_mbps}Mb/" - f"{source_profile.duplex}/autoneg-on" - ) - else: - cmd.extend(["autoneg", "on"]) - change_summary = f"{target_ifname} link=autoneg-on" - elif source_profile.autoneg is False and source_profile.speed_mbps is not None and source_profile.duplex is not None: - cmd.extend( - [ - "speed", - str(source_profile.speed_mbps), - "duplex", - source_profile.duplex, - "autoneg", - "off", - ] ) - change_summary = ( - f"{target_ifname} link={source_profile.speed_mbps}Mb/" - f"{source_profile.duplex}/autoneg-off" + commands.append( + ( + [_ETHTOOL_BIN, "-s", target_ifname, "autoneg", "on"], + f"{target_ifname} link=autoneg-on-safe", + ) ) else: return None - try: - subprocess.run(cmd, capture_output=True, text=True, check=True) - except (FileNotFoundError, subprocess.CalledProcessError) as exc: - logger.debug( - "Failed to synchronize ethtool profile from %s to %s on bridge=%s: %s", - source_ifname, - target_ifname, - self.bridge_name, - exc, - ) + last_error: Optional[Exception] = None + for cmd, summary in commands: + try: + subprocess.run(cmd, capture_output=True, text=True, check=True) + change_summary = summary + break + except (FileNotFoundError, subprocess.CalledProcessError) as exc: + last_error = exc + logger.debug( + "Failed to synchronize ethtool profile from %s to %s on bridge=%s with %s: %s", + source_ifname, + target_ifname, + self.bridge_name, + cmd, + exc, + ) + + if change_summary is None: + if last_error is not None: + self._set_last_action( + f"failed to sync link profile from {source_ifname} to {target_ifname}: {last_error}" + ) return None self._mark_managed_change(target_ifname) diff --git a/frontend/src/pages/Network.tsx b/frontend/src/pages/Network.tsx index db4e8f5..682d224 100644 --- a/frontend/src/pages/Network.tsx +++ b/frontend/src/pages/Network.tsx @@ -1,4 +1,4 @@ -import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; +import { DeleteOutlined, RedoOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { Button, Card, @@ -242,7 +242,12 @@ export default function Network() { okText="Reset" cancelText="Cancel" > - +