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"
>
-
+ }
+ loading={resetBusyInterface === record.name}
+ aria-label={`Reset ${record.name} to defaults`}
+ title={`Reset ${record.name} to defaults`}
+ />
),
},