From ab4112e8b98deab62fc3631d0844c6af2ef86ab7 Mon Sep 17 00:00:00 2001 From: malmert Date: Mon, 9 Mar 2026 16:58:53 +0100 Subject: [PATCH] test config macthing --- .../utilities/bridge_link_state_manager.py | 74 ++++++++++++++++++- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/backend/src/utilities/bridge_link_state_manager.py b/backend/src/utilities/bridge_link_state_manager.py index 6635e78..0698f40 100644 --- a/backend/src/utilities/bridge_link_state_manager.py +++ b/backend/src/utilities/bridge_link_state_manager.py @@ -204,6 +204,7 @@ class BridgeLinkStateWatcher: now = time.time() with self._lock: + previous_states = dict(self._member_states) self._last_event_ts = now self._last_error = None self._member_states = states @@ -247,7 +248,7 @@ class BridgeLinkStateWatcher: ) if failing_members: - config_source = self._find_config_sync_source(states, preferred_ifname=source_ifname) + config_source = self._find_config_sync_source(states, previous_states, preferred_ifname=source_ifname) if config_source is not None and self._has_config_mismatch(config_source, states): with self._lock: self._failing_since = {} @@ -290,8 +291,9 @@ class BridgeLinkStateWatcher: self._failing_since = {} self._all_clear_since = None - if source_ifname in states and self._is_source_eligible(source_ifname, states): - self._sync_member_configuration(source_ifname, states) + config_source = self._find_config_sync_source(states, previous_states, preferred_ifname=source_ifname) + if config_source is not None and self._has_config_mismatch(config_source, states): + self._sync_member_configuration(config_source, states) return self._set_last_action(f"no_restore_needed ({reason})") @@ -323,20 +325,84 @@ class BridgeLinkStateWatcher: def _find_config_sync_source( self, states: dict[str, MemberLinkState], + previous_states: dict[str, MemberLinkState], preferred_ifname: Optional[str] = None, ) -> Optional[str]: """Pick a healthy member whose configuration should be mirrored to siblings.""" + changed_candidates = [ + ifname + for ifname in sorted(states) + if self._config_changed(ifname, states, previous_states) + ] + if preferred_ifname in changed_candidates: + changed_candidates.remove(preferred_ifname) + changed_candidates.insert(0, preferred_ifname) + + if changed_candidates: + for ifname in changed_candidates: + if self._is_changed_source_eligible(ifname, states): + return ifname + self._set_last_action( + f"waiting for changed configuration on {changed_candidates} to settle before syncing" + ) + return None + candidates: list[str] = [] if preferred_ifname: candidates.append(preferred_ifname) candidates.extend(ifname for ifname in sorted(states) if ifname != preferred_ifname) + seen: set[str] = set() for ifname in candidates: + if ifname in seen: + continue + seen.add(ifname) if self._is_source_eligible(ifname, states): return ifname return None + def _is_changed_source_eligible(self, ifname: str, states: dict[str, MemberLinkState]) -> bool: + """Allow a changed member to drive sync even while the link is transiently renegotiating.""" + with self._lock: + ignored = self._managed_event_deadlines.get(ifname, 0.0) > time.time() + suppressed = ifname in self._suppressed_members + if ignored or suppressed: + return False + + state = states.get(ifname) + if state is None: + return False + if state.link_ready: + return True + return self._state_has_usable_config(state) + + def _config_changed( + self, + ifname: str, + states: dict[str, MemberLinkState], + previous_states: dict[str, MemberLinkState], + ) -> bool: + """Return whether this member's MTU or link profile changed since the last snapshot.""" + current = states.get(ifname) + previous = previous_states.get(ifname) + if current is None or previous is None: + return False + + return current.mtu != previous.mtu or current.ethernet_profile != previous.ethernet_profile + + def _state_has_usable_config(self, state: MemberLinkState) -> bool: + """Return whether this snapshot contains configuration that can be mirrored.""" + if state.mtu is not None: + return True + + profile = state.ethernet_profile + if profile is None: + return False + if profile.autoneg is True: + return True + return profile.autoneg is False and profile.speed_mbps is not None and profile.duplex is not None + def _has_config_mismatch(self, source_ifname: str, states: dict[str, MemberLinkState]) -> bool: """Return whether any sibling differs from the chosen source configuration.""" source = states.get(source_ifname) @@ -348,7 +414,7 @@ class BridgeLinkStateWatcher: continue if source.mtu is not None and target.mtu is not None and source.mtu != target.mtu: return True - if source.ethernet_profile is not None and target.ethernet_profile is not None and source.ethernet_profile != target.ethernet_profile: + if source.ethernet_profile is not None and source.ethernet_profile != target.ethernet_profile: return True return False