diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue index 85b18b97b8..e436eb3a95 100644 --- a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -140,16 +140,19 @@ export default Vue.extend({ await Promise.all(interfaces.map((iface) => this.checkInterfaceInternet(host, iface))) }, async setHighestInterface(): Promise { - 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}` notifier.pushError('INCREASE_NETWORK_INTERFACE_METRIC_FAIL', message) }) .then(() => { @@ -161,7 +164,6 @@ export default Vue.extend({ this.close() }) await this.fetchAvailableInterfaces() - this.is_loading = false }, async fetchAvailableInterfaces(): Promise { await back_axios({ @@ -173,9 +175,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 }) diff --git a/core/frontend/src/types/ethernet.ts b/core/frontend/src/types/ethernet.ts index 8aa5ecda8e..7f0df01f23 100644 --- a/core/frontend/src/types/ethernet.ts +++ b/core/frontend/src/types/ethernet.ts @@ -19,4 +19,5 @@ export interface EthernetInterface { name: string, addresses: InterfaceAddress[], info?: InterfaceInfo, + priority?: number, } diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index e449463fc5..a811dc0084 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -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"]: @@ -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: @@ -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) @@ -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) @@ -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 @@ -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] @@ -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() @@ -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: @@ -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. @@ -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)) @@ -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 @@ -572,7 +591,7 @@ 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. @@ -580,14 +599,21 @@ def priorities_mismatch(self) -> bool: 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. @@ -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") @@ -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) diff --git a/core/services/cable_guy/networksetup.py b/core/services/cable_guy/networksetup.py index 38475c0cbb..986dfbb2b5 100755 --- a/core/services/cable_guy/networksetup.py +++ b/core/services/cable_guy/networksetup.py @@ -1,7 +1,7 @@ import os import re import socket -from typing import Dict, List +from typing import Any, List import sdbus from loguru import logger @@ -14,7 +14,7 @@ NetworkManagerSettings, ) -from typedefs import NetworkInterfaceMetric, NetworkInterfaceMetricApi +from typedefs import NetworkInterfaceMetricApi sdbus.set_default_bus(sdbus.sd_bus_open_system()) @@ -28,9 +28,6 @@ def __init__(self) -> None: def detect(self) -> bool: raise NotImplementedError("NetworkManager does not support detecting network interfaces priority") - def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: - raise NotImplementedError("NetworkManager does not support getting network interfaces priority") - def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: raise NotImplementedError("NetworkManager does not support setting interface priority") @@ -49,6 +46,83 @@ def trigger_dynamic_ip_acquisition(self, interface_name: str) -> None: def cleanup_interface_connections(self, interface_name: str) -> None: pass + def _update_route(self, interface_name: str, interface_index: int, route: Any, priority: int) -> None: + """Update a single route with new priority. + + Args: + interface_name: Name of the interface + interface_index: Index of the interface + route: Route object + priority: New priority to set + """ + # Skip non-default routes (those not pointing to 0.0.0.0/0) + dst = route.get_attr("RTA_DST") + if dst is not None: # If dst is None, it's a default route (0.0.0.0/0) + return + if route.get_attr("RTA_PRIORITY") == priority: + return + + self.ipr.route( + "del", + oif=interface_index, + family=socket.AF_INET, + scope=route["scope"], + proto=route["proto"], + type=route["type"], + dst="0.0.0.0/0", # For default route + gateway=route.get_attr("RTA_GATEWAY"), + table=route.get_attr("RTA_TABLE", 254), # Default to main table if not specified + ) + + for attempt in range(3): + try: + # Add the new route with updated priority + logger.info(f"Adding new route for {interface_name} with priority {priority}") + self.ipr.route( + "add", + oif=interface_index, + family=socket.AF_INET, + scope=route["scope"], + proto=route["proto"], + type=route["type"], + dst="0.0.0.0/0", # For default route + gateway=route.get_attr("RTA_GATEWAY"), + priority=priority + attempt, + table=route.get_attr("RTA_TABLE", 254), # Default to main table if not specified + ) + logger.info(f"Updated default route for {interface_name} with priority {priority}") + break + except Exception as e: + logger.error(f"Failed to update route for {interface_name}: {e} (attempt {attempt})") + + def set_interfaces_priority_using_ipr(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: + if not interfaces: + logger.info("No interfaces to set priority for") + return + + for interface in interfaces: + try: + # Use specified priority or increment current_priority + priority = interface.priority + + # Get interface index + interface_index = self.ipr.link_lookup(ifname=interface.name)[0] + + # Get all routes for this interface + routes = self.ipr.get_routes(oif=interface_index, family=socket.AF_INET) + + # Update existing routes + for route in routes: + try: + self._update_route(interface.name, interface_index, route, priority) + except Exception as e: + logger.error(f"Failed to update route for {interface.name}: {e}") + continue + + except Exception as e: + logger.error(f"Failed to set priority for interface {interface.name}: {e}") + continue + class BookwormHandler(AbstractNetworkHandler): """ @@ -61,6 +135,9 @@ def cleanup_interface_connections(self, interface_name: str) -> None: network_manager_settings = NetworkManagerSettings() for connection_path in network_manager_settings.connections: profile = NetworkConnectionSettings(connection_path).get_profile() + # Skip if this is a wireless connection + if profile.connection.connection_type == "802-11-wireless": + continue if profile.connection.interface_name == interface_name: logger.info( f"Removing connection {profile.connection.uuid} ({profile.connection.connection_id}) for interface {interface_name}" @@ -106,40 +183,6 @@ def add_static_ip(self, interface_name: str, ip: str) -> None: except Exception as error: logger.error(f"Failed to add IP '{ip}' to interface '{interface_name}'. {error}") - def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: - """Get the priority metrics for all network interfaces using IPRoute. - - Returns: - List[NetworkInterfaceMetric]: A list of priority metrics for each interface. - """ - interfaces = self.ipr.get_links() - # Get all IPv4 routes - routes = self.ipr.get_routes(family=socket.AF_INET) - - # Create a mapping of interface index to name - name_dict = {iface["index"]: iface.get_attr("IFLA_IFNAME") for iface in interfaces} - - # Get metrics for each interface from routes - 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 - 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"] - - # Create NetworkInterfaceMetric objects for each interface - result = [] - for index, name in name_dict.items(): - metric = NetworkInterfaceMetric(index=index, name=name, priority=metric_dict.get(index, 0)) - result.append(metric) - - return result - # pylint: disable=too-many-nested-blocks def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: """Sets network interface priority using IPRoute. @@ -148,58 +191,7 @@ def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) - interfaces (List[NetworkInterfaceMetricApi]): A list of interfaces and their priority metrics, sorted by priority to set. """ - if not interfaces: - logger.info("No interfaces to set priority for") - return - - # If there's only one interface and no priority specified, set it to highest priority (0) - if len(interfaces) == 1 and interfaces[0].priority is None: - interfaces[0].priority = 0 - - current_priority = 1000 - for interface in interfaces: - try: - # Use specified priority or increment current_priority - priority = interface.priority if interface.priority is not None else current_priority - - # Get interface index - interface_index = self.ipr.link_lookup(ifname=interface.name)[0] - - # Get all routes for this interface - routes = self.ipr.get_routes(oif=interface_index, family=socket.AF_INET) - - # Update existing routes - for route in routes: - try: - route_data = {"priority": priority} - - # Copy existing route attributes - for attr in ["RTA_DST", "RTA_GATEWAY", "RTA_TABLE", "RTA_PREFSRC"]: - value = route.get_attr(attr) - if value: - # Convert attribute name to lowercase and remove RTA_ prefix - key = attr.lower().replace("rta_", "") - route_data[key] = value - - # Update the route - self.ipr.route( - "replace", - oif=interface_index, - family=socket.AF_INET, - scope=route["scope"], - proto=route["proto"], - type=route["type"], - **route_data, - ) - logger.info(f"Updated route for {interface.name} with priority {priority}") - except Exception as e: - logger.error(f"Failed to update route for {interface.name}: {e}") - continue - - current_priority += 1000 - except Exception as e: - logger.error(f"Failed to set priority for interface {interface.name}: {e}") - continue + self.set_interfaces_priority_using_ipr(interfaces) def _get_dhcp_address_using_dhclient(self, interface_name: str) -> str | None: """Run dhclient to get a new IP address and return it. @@ -275,7 +267,6 @@ class DHCPCD(AbstractNetworkHandler): dhcpcd_conf_start_string = "#blueos-interface-priority-start" dhcpcd_conf_end_string = "#blueos-interface-priority-end" # https://man.archlinux.org/man/dhcpcd.conf.5#metric - default_dhcpdc_metric = 1000 def detect(self) -> bool: return os.path.isfile("/etc/dhcpcd.conf") @@ -312,42 +303,6 @@ def _get_service_dhcpcd_content(self) -> List[str]: logger.warning(f"Failed to read {self.dhcpcd_conf_path}, error: {exception}") return [] - def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: - """Parses dhcpcd config file to get network interface priorities. - Goes through the dhcpcd config file line by line looking for "interface" - and "metric" lines. Extracts the interface name and metric value. The - metric is used as the priority, with lower being better. - - List[NetworkInterfaceMetric]: A list of priority metrics for each interface. - """ - lines = self._get_service_dhcpcd_content() - result = [] - current_interface = None - current_metric = None - for line in lines: - if line.startswith("interface"): - if current_interface is not None and current_metric is not None: # type: ignore[unreachable] - # Metric is inverted compared to priority, lowest metric wins - result.append(NetworkInterfaceMetric(index=0, name=current_interface, priority=current_metric)) # type: ignore[unreachable] - - current_interface = line.split()[1] - current_metric = None - - elif line.startswith("metric") and current_interface is not None: - try: - current_metric = int(line.split()[1]) - except Exception as exception: - logger.error( - f"Failed to parse {current_interface} metric, error: {exception}, line: {line}, using default metric" - ) - current_metric = self.default_dhcpdc_metric - - # Add the last entry to the result_list - if current_interface is not None and current_metric is not None: - result.append(NetworkInterfaceMetric(index=0, name=current_interface, priority=current_metric)) - - return result - def _remove_dhcpcd_configuration(self) -> None: """Removes the network priority configuration added by this service from dhcpcd.conf file. @@ -379,8 +334,46 @@ def _remove_dhcpcd_configuration(self) -> None: with open("/etc/dhcpcd.conf", "w", encoding="utf-8") as f: f.writelines(lines) + def _get_dhcp_address_using_dhclient(self, interface_name: str) -> str | None: + """Run dhclient to get a new IP address and return it. + + Args: + interface_name: Name of the interface to get IP for + + Returns: + The IP address acquired from DHCP, or None if failed + """ + try: + # Just run dhclient without releasing existing IPs + command = f"timeout 5 dhclient -v {interface_name} 2>&1" + logger.info(f"Running: {command}") + dhclient_output = os.popen(command).read() + + bound_ip_match = re.search(r"bound to ([0-9.]+)", dhclient_output) + if bound_ip_match: + return bound_ip_match.group(1) + + logger.error(f"Could not find bound IP in dhclient output: {dhclient_output}") + return None + + except Exception as e: + logger.error(f"Failed to run dhclient: {e}") + return None + def trigger_dynamic_ip_acquisition(self, interface_name: str) -> None: - raise NotImplementedError("This Handler does not support triggering dynamic IP acquisition") + """Get a new IP from DHCP using dhclient. + The IP will be managed by dhclient and not added to NetworkManager's configuration. + + Args: + interface_name: Name of the interface to get IP for + """ + # Get new IP using dhclient + new_ip = self._get_dhcp_address_using_dhclient(interface_name) + if not new_ip: + logger.error(f"Failed to get DHCP-acquired IP for {interface_name}") + return + + logger.info(f"Got new IP {new_ip} from DHCP for {interface_name}") def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: """Sets network interface priority.. @@ -389,7 +382,8 @@ def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) - interfaces (List[NetworkInterfaceMetricApi]): A list of interfaces and their priority metrics. """ - # Note: With DHCPCD, lower priority wins! + self.set_interfaces_priority_using_ipr(interfaces) + self._remove_dhcpcd_configuration() # Update interfaces priority if possible @@ -397,11 +391,7 @@ def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) - logger.info("Cant change network priority from empty list.") return - # If there is a single interface without metric, make it the highest priority - if len(interfaces) == 1 and interfaces[0].priority is None: - interfaces[0].priority = 0 - - current_priority = interfaces[0].priority or self.default_dhcpdc_metric + current_priority = interfaces[0].priority lines = [] lines.append(f"{self.dhcpcd_conf_start_string}\n") for interface in interfaces: diff --git a/core/services/cable_guy/typedefs.py b/core/services/cable_guy/typedefs.py index 7caa0205ee..c174ca9885 100755 --- a/core/services/cable_guy/typedefs.py +++ b/core/services/cable_guy/typedefs.py @@ -31,6 +31,7 @@ class NetworkInterface(BaseModel): name: str addresses: List[InterfaceAddress] info: Optional[InterfaceInfo] + priority: Optional[int] def __hash__(self) -> int: return hash(self.name) + sum(hash(address) for address in self.addresses) @@ -44,4 +45,4 @@ class NetworkInterfaceMetric(BaseModel): class NetworkInterfaceMetricApi(BaseModel): name: str - priority: Optional[int] + priority: int