Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix priority changing on Bookworm #3093

Merged
merged 8 commits into from
Feb 10, 2025
19 changes: 11 additions & 8 deletions core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,20 @@ export default Vue.extend({
await Promise.all(interfaces.map((iface) => this.checkInterfaceInternet(host, iface)))
},
async setHighestInterface(): Promise<void> {
this.is_loading = true
const interface_priority = this.interfaces.map((inter) => ({ name: inter.name }))
const interface_priorities = this.interfaces.map((inter) => ({ name: inter.name, priority: 0 }))
interface_priorities.forEach((inter, index) => {
inter.priority = index * 1000
})
this.interfaces = []
await back_axios({
method: 'post',
url: `${ethernet.API_URL}/set_interfaces_priority`,
timeout: 10000,
data: interface_priority,
data: interface_priorities,
})
.catch((error) => {
const message = `Could not increase the priority for interface '${interface_priority}', ${error}.`
const message = `Could not set network interface priorities: ${interface_priorities}, error: ${error}`
console.log(interface_priorities)
Williangalvani marked this conversation as resolved.
Show resolved Hide resolved
notifier.pushError('INCREASE_NETWORK_INTERFACE_METRIC_FAIL', message)
})
.then(() => {
Expand All @@ -161,7 +165,6 @@ export default Vue.extend({
this.close()
})
await this.fetchAvailableInterfaces()
this.is_loading = false
},
async fetchAvailableInterfaces(): Promise<void> {
await back_axios({
Expand All @@ -173,9 +176,9 @@ export default Vue.extend({
.then((response) => {
const interfaces = response.data as EthernetInterface[]
interfaces.sort((a, b) => {
if (!a.info) return -1
if (!b.info) return 1
return a.info.priority - b.info.priority
const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER
const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER
return priorityA - priorityB
})
this.interfaces = interfaces
})
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/types/ethernet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export interface EthernetInterface {
name: string,
addresses: InterfaceAddress[],
info?: InterfaceInfo,
priority?: number,
}
156 changes: 93 additions & 63 deletions core/services/cable_guy/api/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,7 @@ def __init__(self, default_configs: List[NetworkInterface]) -> None:
# Load settings and do the initial configuration
if not self.settings.load():
logger.error(f"Failed to load previous settings. Using default configuration: {default_configs}")
try:
for config in default_configs:
self.set_configuration(config)
except Exception as error:
logger.error(f"Failed loading default configuration. {error}")
return
self.settings.root = {"content": [entry.dict() for entry in default_configs]}

logger.info("Loading previous settings.")
for item in self.settings.root["content"]:
Expand All @@ -85,14 +80,12 @@ def set_configuration(self, interface: NetworkInterface, watchdog_call: bool = F
"""
if not watchdog_call:
self.network_handler.cleanup_interface_connections(interface.name)
interfaces = self.get_ethernet_interfaces()
interfaces = self.get_interfaces()
valid_names = [interface.name for interface in interfaces]
if interface.name not in valid_names:
raise ValueError(f"Invalid interface name ('{interface.name}'). Valid names are: {valid_names}")

logger.info(f"Setting configuration for interface '{interface.name}'.")
# Reset the interface by removing all IPs and DHCP servers associated with it
self.flush_interface(interface.name)
self.remove_dhcp_server_from_interface(interface.name)

if interface.addresses:
Expand Down Expand Up @@ -289,6 +282,14 @@ def add_static_ip(self, interface_name: str, ip: str, mode: AddressMode = Addres
logger.error(f"Failed to add IP '{ip}' to interface '{interface_name}'. {error}")

saved_interface = self.get_saved_interface_by_name(interface_name)
if saved_interface is None:
# If the interface is not saved, create a new one
saved_interface = NetworkInterface(
name=interface_name,
addresses=[
InterfaceAddress(ip=ip, mode=AddressMode.Unmanaged),
],
)
new_address = InterfaceAddress(ip=ip, mode=mode)
if new_address not in saved_interface.addresses:
saved_interface.addresses.append(new_address)
Expand All @@ -313,6 +314,9 @@ def remove_ip(self, interface_name: str, ip_address: str) -> None:
raise RuntimeError(f"Cannot delete IP '{ip_address}' from interface {interface_name}.") from error

saved_interface = self.get_saved_interface_by_name(interface_name)
if saved_interface is None:
logger.error(f"Interface {interface_name} is not managed by Cable Guy. Not deleting IP {ip_address}.")
return
saved_interface.addresses = [address for address in saved_interface.addresses if address.ip != ip_address]
self._update_interface_settings(interface_name, saved_interface)

Expand All @@ -322,12 +326,13 @@ def get_interface_by_name(self, name: str) -> NetworkInterface:
return interface
raise ValueError(f"No interface with name '{name}' is present.")

def get_saved_interface_by_name(self, name: str) -> NetworkInterface:
def get_saved_interface_by_name(self, name: str) -> Optional[NetworkInterface]:
for interface in self.settings.root["content"]:
if interface["name"] == name:
return NetworkInterface(**interface)
return self.get_interface_by_name(name)
return None

# pylint: disable=too-many-locals
def get_interfaces(self, filter_wifi: bool = False) -> List[NetworkInterface]:
"""Get interfaces information

Expand Down Expand Up @@ -365,9 +370,18 @@ def get_interfaces(self, filter_wifi: bool = False) -> List[NetworkInterface]:
else:
mode = AddressMode.Unmanaged if is_static_ip and valid_ip else AddressMode.Client
valid_addresses.append(InterfaceAddress(ip=ip, mode=mode))

info = self.get_interface_info(interface)
interface_data = NetworkInterface(name=interface, addresses=valid_addresses, info=info)
saved_interface = self.get_saved_interface_by_name(interface)
# Get priority from saved interface or from current interface metrics, defaulting to None if neither exists
priority = None
if saved_interface and saved_interface.priority is not None:
priority = saved_interface.priority
else:
interface_metric = self.get_interface_priority(interface)
if interface_metric:
priority = interface_metric.priority

interface_data = NetworkInterface(name=interface, addresses=valid_addresses, info=info, priority=priority)
# Check if it's valid and add to the result
if self.validate_interface_data(interface_data, filter_wifi):
result += [interface_data]
Expand Down Expand Up @@ -400,17 +414,13 @@ def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]:
Returns:
List[NetworkInterfaceMetric]: List of interface priorities, lower is higher priority
"""
result = self.network_handler.get_interfaces_priority()
if result:
return result

return self._get_interfaces_priority_from_ipr()

def _get_interfaces_priority_from_ipr(self) -> List[NetworkInterfaceMetric]:
"""Get the priority metrics for all network interfaces.
"""Get the priority metrics for all network interfaces that are UP and RUNNING.

Returns:
List[NetworkInterfaceMetric]: A list of priority metrics for each interface.
List[NetworkInterfaceMetric]: A list of priority metrics for each active interface.
"""

interfaces = self.ipr.get_links()
Expand All @@ -420,36 +430,36 @@ def _get_interfaces_priority_from_ipr(self) -> List[NetworkInterfaceMetric]:
# GLHF
routes = self.ipr.get_routes(family=AddressFamily.AF_INET)

# Generate a dict of index to network name.
# And a second list between the network index and metric,
# keep in mind that a single interface can have multiple routes
name_dict = {iface["index"]: iface.get_attr("IFLA_IFNAME") for iface in interfaces}
metric_index_list = [
{"metric": route.get_attr("RTA_PRIORITY", 0), "index": route.get_attr("RTA_OIF")} for route in routes
]

# Keep the highest metric per interface in a dict of index to metric
metric_dict: Dict[int, int] = {}
for d in metric_index_list:
if d["index"] in metric_dict:
metric_dict[d["index"]] = max(metric_dict[d["index"]], d["metric"])
else:
metric_dict[d["index"]] = d["metric"]

# Highest priority wins for ipr but not for dhcpcd, so we sort and reverse the list
# Where we change the priorities between highest and low to convert that
original_list = sorted(
[
NetworkInterfaceMetric(index=index, name=name, priority=metric_dict.get(index) or 0)
for index, name in name_dict.items()
],
key=lambda x: x.priority,
reverse=True,
)

# First find interfaces with default routes
interfaces_with_default_routes = set()
for route in routes:
dst = route.get_attr("RTA_DST")
oif = route.get_attr("RTA_OIF")
if dst is None and oif is not None: # Default route
interfaces_with_default_routes.add(oif)

# Generate a dict of index to network name, but only for interfaces that are UP and RUNNING
# IFF_UP flag is 0x1, IFF_RUNNING is 0x40 in Linux
name_dict = {
iface["index"]: iface.get_attr("IFLA_IFNAME")
for iface in interfaces
if (iface["flags"] & 0x1) and (iface["flags"] & 0x40) and iface["index"] in interfaces_with_default_routes
}

# Get metrics for default routes of active interfaces
interface_metrics: Dict[int, int] = {}
for route in routes:
oif = route.get_attr("RTA_OIF")
if oif in name_dict and route.get_attr("RTA_DST") is None: # Only default routes
metric = route.get_attr("RTA_PRIORITY", 0)
# Keep the highest metric for each interface
if oif not in interface_metrics or metric > interface_metrics[oif]:
interface_metrics[oif] = metric

# Create the list of interface metrics
return [
NetworkInterfaceMetric(index=item.index, name=item.name, priority=original_list[-(i + 1)].priority)
for i, item in enumerate(original_list)
NetworkInterfaceMetric(index=index, name=name, priority=interface_metrics.get(index, 0))
for index, name in name_dict.items()
]

def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None:
Expand All @@ -461,6 +471,13 @@ def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -
sorted by priority to set, if values are undefined.
"""
self.network_handler.set_interfaces_priority(interfaces)
# save to settings
for interface in interfaces:
saved_interface = self.get_saved_interface_by_name(interface.name)
if saved_interface is None:
saved_interface = NetworkInterface(name=interface.name, addresses=[], priority=interface.priority)
saved_interface.priority = interface.priority
self._update_interface_settings(interface.name, saved_interface)

def get_interface_priority(self, interface_name: str) -> Optional[NetworkInterfaceMetric]:
"""Get the priority metric for a network interface.
Expand Down Expand Up @@ -532,7 +549,7 @@ def _is_dhcp_server_running_on_interface(self, interface_name: str) -> bool:
except Exception:
return False

def remove_dhcp_server_from_interface(self, interface_name: str) -> DHCPServerManager:
def remove_dhcp_server_from_interface(self, interface_name: str) -> None:
logger.info(f"Removing DHCP server from interface '{interface_name}'.")
try:
self._dhcp_servers.remove(self._dhcp_server_on_interface(interface_name))
Expand All @@ -543,6 +560,8 @@ def remove_dhcp_server_from_interface(self, interface_name: str) -> DHCPServerMa
raise RuntimeError("Cannot remove DHCP server from interface.") from error

saved_interface = self.get_saved_interface_by_name(interface_name)
if saved_interface is None:
return
# Get all non-server addresses
new_ip_list = [address for address in saved_interface.addresses if address.mode != AddressMode.Server]
# Find the server address if it exists and convert it to unmanaged
Expand Down Expand Up @@ -572,22 +591,29 @@ def stop(self) -> None:
def __del__(self) -> None:
self.stop()

def priorities_mismatch(self) -> bool:
def priorities_mismatch(self) -> List[NetworkInterface]:
"""Check if the current interface priorities differ from the saved ones.
Uses sets for order-independent comparison of NetworkInterfaceMetric objects,
which compare only name and priority fields.

Returns:
bool: True if priorities don't match, False if they do
"""
if "priorities" not in self.settings.root:
return False

current = set(self.get_interfaces_priority())
# Convert saved priorities to NetworkInterfaceMetric, index value doesn't matter for comparison
saved = {NetworkInterfaceMetric(index=0, **iface) for iface in self.settings.root["priorities"]}
mismatched_interfaces = []
current_priorities = {interface.name: interface.priority for interface in self.get_interfaces_priority()}

for interface_settings in self.settings.root["content"]:
interface = NetworkInterface(**interface_settings)
if interface.priority is None:
continue
if interface.name in current_priorities and interface.priority != current_priorities[interface.name]:
logger.info(
f"Priority mismatch for {interface.name}: {interface.priority} != {current_priorities[interface.name]}"
)
mismatched_interfaces.append(interface)

return current != saved
return mismatched_interfaces

def config_mismatch(self) -> Set[NetworkInterface]:
"""Check if the current interface config differs from the saved ones.
Expand All @@ -611,7 +637,7 @@ def config_mismatch(self) -> Set[NetworkInterface]:
logger.debug(f"Interface {interface.name} not in saved configuration, skipping")
continue

for address in self.get_saved_interface_by_name(interface.name).addresses:
for address in saved_interfaces[interface.name].addresses:
if address.mode == AddressMode.Client:
if not any(addr.mode == AddressMode.Client for addr in interface.addresses):
logger.info(f"Mismatch detected for {interface.name}: missing dhcp client address")
Expand All @@ -633,16 +659,20 @@ async def watchdog(self) -> None:
if there is a mismatch, it will apply the saved settings
"""
while True:
if self.priorities_mismatch():
logger.warning("Interface priorities mismatch, applying saved settings.")
try:
self.set_interfaces_priority(self.settings.root["priorities"])
except Exception as error:
logger.error(f"Failed to set interface priorities: {error}")
mismatches = self.config_mismatch()
if mismatches:
logger.warning("Interface config mismatch, applying saved settings.")
logger.debug(f"Mismatches: {mismatches}")
for interface in mismatches:
self.set_configuration(interface, watchdog_call=True)
priority_mismatch = self.priorities_mismatch()
if priority_mismatch:
logger.warning("Interface priorities mismatch, applying saved settings.")
saved_interfaces = self.settings.root["content"]
priorities = [
NetworkInterfaceMetricApi(name=interface["name"], priority=interface["priority"])
for interface in saved_interfaces
if "priority" in interface and interface["priority"] is not None
]
self.set_interfaces_priority(priorities)
await asyncio.sleep(5)
Loading