From 48fd09239b618cd5210ac48445472660f88f4568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 26 Jul 2023 13:35:29 -0300 Subject: [PATCH 1/8] core: services: cable_guy: Move interface priority from ipr to dhcpcd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/services/cable_guy/api/manager.py | 183 ++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 4 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index 444965fecc..b6499a37bf 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -60,9 +60,16 @@ class EthernetManager: # DNS abstraction dns = dns.Dns() + # https://man.archlinux.org/man/dhcpcd.conf.5#metric + default_dhcpdc_metric = 1000 + result: List[NetworkInterface] = [] def __init__(self, default_configs: List[NetworkInterface]) -> None: + self.dhcpcd_conf_path = "/etc/dhcpcd.conf" + self.dhcpcd_conf_start_string = "#blueos-interface-priority-start" + self.dhcpcd_conf_end_string = "#blueos-interface-priority-end" + self.settings = settings.Settings() self._dhcp_servers: List[DHCPServerManager] = [] @@ -367,6 +374,18 @@ def get_interface_ndb(self, interface_name: str) -> Any: @temporary_cache(timeout_seconds=5) def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: + """Get priority of network interfaces dhcpcd otherwise fetch from ipr. + + Returns: + List[NetworkInterfaceMetric]: List of interface priorities, lower is higher priority + """ + result = self._get_interface_priority_from_dhcpcd() + 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. Returns: @@ -396,11 +415,167 @@ def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: 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, + ) + return [ - NetworkInterfaceMetric(index=index, name=name, priority=metric_dict.get(index) or 0) - for index, name in name_dict.items() + NetworkInterfaceMetric(index=item.index, name=item.name, priority=original_list[-(i + 1)].priority) + for i, item in enumerate(original_list) ] + def _get_service_dhcpcd_content(self) -> List[str]: + """Returns a list of lines from the dhcpcd configuration file that belong to + this service. + Any exceptions are caught and logged, and an empty list is returned. + + List[str]: Lines that will be used by this service + """ + try: + with open(self.dhcpcd_conf_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + start, end = None, None + for i, line in enumerate(lines): + # Get always the first occurrence of 'start' and last of 'end' + if self.dhcpcd_conf_start_string in line and start is None: + start = i + if self.dhcpcd_conf_end_string in line: + end = i + + # Remove everything that is not from us + if start is not None and end is not None: + del lines[0 : start + 1] + del lines[end:-1] + + # Clean all lines and remove empty ones + lines = [line.strip() for line in lines] + lines = [line for line in lines if line] + return lines + except Exception as exception: + logger.warning(f"Failed to read {self.dhcpcd_conf_path}, error: {exception}") + return [] + + def _get_interface_priority_from_dhcpcd(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 = EthernetManager.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. + """ + lines = [] + with open(self.dhcpcd_conf_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + start, end = None, None + for i, line in enumerate(lines): + # Get always the first occurrence of 'start' and last of 'end' + if self.dhcpcd_conf_start_string in line and start is None: + start = i + if self.dhcpcd_conf_end_string in line: + end = i + + # Remove our part + if start is not None and end is not None: + logger.info(f"Deleting rage: {start} : {end + 1}") + del lines[start : end + 1] + else: + logger.info(f"There is no network priority configuration in {self.dhcpcd_conf_path}") + return + + if not lines: + logger.warning(f"{self.dhcpcd_conf_path} appears to be empty.") + return + + with open("/etc/dhcpcd.conf", "w", encoding="utf-8") as f: + f.writelines(lines) + + def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: + """Sets network interface priority. This is an abstraction function for different + implementations. + + Args: + interfaces (List[NetworkInterfaceMetricApi]): A list of interfaces and their priority metrics. + sorted by priority to set, if values are undefined. + """ + self._set_interfaces_priority_to_dhcpcd(interfaces) + + def _set_interfaces_priority_to_dhcpcd(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: + """Sets network interface priority.. + + Args: + interfaces (List[NetworkInterfaceMetricApi]): A list of interfaces and their priority metrics. + """ + + # Note: With DHCPCD, lower priority wins! + self._remove_dhcpcd_configuration() + + # Update interfaces priority if possible + if not interfaces: + 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 EthernetManager.default_dhcpdc_metric + lines = [] + lines.append(f"{self.dhcpcd_conf_start_string}\n") + for interface in interfaces: + # Enforce priority if it's none, otherwise track new priority + interface.priority = interface.priority or current_priority + current_priority = interface.priority + + lines.append(f"interface {interface.name}\n") + lines.append(f" metric {interface.priority}\n") + current_priority += 1000 + logger.info(f"Set current priority for {interface.name} as {interface.priority}") + lines.append(f"{self.dhcpcd_conf_end_string}\n") + + with open("/etc/dhcpcd.conf", "a+", encoding="utf-8") as f: + f.writelines(lines) + def get_interface_priority(self, interface_name: str) -> Optional[NetworkInterfaceMetric]: """Get the priority metric for a network interface. @@ -417,8 +592,8 @@ def get_interface_priority(self, interface_name: str) -> Optional[NetworkInterfa return None - def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: - """Set interfaces priority. + def _set_interfaces_priority_to_ipr(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: + """Set interfaces priority with ipr. Args: interfaces (List[NetworkInterfaceMetricApi]): A list of interfaces and their priority metrics. From 79913fe8aa8f3ad6c3aef92b37b002df266f8887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 9 Aug 2023 16:51:30 -0300 Subject: [PATCH 2/8] core: services: cable_guy: manager: Remove _set_interfaces_priority_to_ipr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/services/cable_guy/api/manager.py | 45 -------------------------- 1 file changed, 45 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index b6499a37bf..f0c011295f 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -592,51 +592,6 @@ def get_interface_priority(self, interface_name: str) -> Optional[NetworkInterfa return None - def _set_interfaces_priority_to_ipr(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: - """Set interfaces priority with ipr. - - Args: - interfaces (List[NetworkInterfaceMetricApi]): A list of interfaces and their priority metrics. - """ - if not interfaces: - logger.info("Cant change network priority from empty list.") - return - - highest_metric = 1 - for interface in self.get_interfaces_priority(): - highest_metric = max(highest_metric, interface.priority) - - # If there is a single interface without metric, make it the highest priority - if len(interfaces) == 1 and interfaces[0].priority is None: - interface = self.get_interface_priority(interfaces[0].name) - original_priority = interface and interface.priority or 0 - for interface in self.get_interfaces_priority(): - highest_metric = max(highest_metric, original_priority) - if original_priority == highest_metric: - logger.info(f"Interface {interfaces[0].name} already has highest priority: {highest_metric}") - return - interfaces[0].priority = highest_metric + 10 - - # Ensure a high value for metric - if highest_metric <= len(interfaces): - highest_metric = 400 - - if all(interface.priority is not None for interface in interfaces): - for interface in interfaces: - EthernetManager.set_interface_priority(interface.name, interface.priority) - return - - # Calculate metric automatically in the case where no metric is provided - if all(interface.priority is None for interface in interfaces): - network_step = int(highest_metric / len(interfaces)) - times = 0 - for interface in interfaces: - EthernetManager.set_interface_priority(interface.name, highest_metric - network_step * times) - times += 1 - return - - raise RuntimeError("There is no support for interfaces with and without metric on the same list") - @staticmethod def set_interface_priority(name: str, priority: int) -> None: """Set interface priority From 1ed37dd39aa1b56c75869ef885c155259f7e8433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 9 Aug 2023 17:07:55 -0300 Subject: [PATCH 3/8] core: frontend: components: NetworkInterfacePriority: Fix list order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../src/components/app/NetworkInterfacePriorityMenu.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue index d41862fdbb..a99292165b 100644 --- a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -105,9 +105,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 b.info.priority - a.info.priority + if (!a.info) return -1 + if (!b.info) return 1 + return a.info.priority - b.info.priority }) this.interfaces = interfaces }) From 409f14efbb6ce9d8d8009b9390965974319908d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 9 Aug 2023 17:08:51 -0300 Subject: [PATCH 4/8] core: services: cable_guy: manager: Decrease cache for quick frontend update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/services/cable_guy/api/manager.py | 2 +- core/services/cable_guy/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index f0c011295f..3cc53867b3 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -372,7 +372,7 @@ def get_interface_ndb(self, interface_name: str) -> Any: """ return self.ndb.interfaces.dump().filter(ifname=interface_name)[0] - @temporary_cache(timeout_seconds=5) + @temporary_cache(timeout_seconds=1) def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: """Get priority of network interfaces dhcpcd otherwise fetch from ipr. diff --git a/core/services/cable_guy/main.py b/core/services/cable_guy/main.py index 33bb5df0ae..8d818a7f6d 100755 --- a/core/services/cable_guy/main.py +++ b/core/services/cable_guy/main.py @@ -80,7 +80,7 @@ def configure_interface(interface: NetworkInterface = Body(...)) -> Any: @app.get("/interfaces", response_model=List[NetworkInterface], summary="Retrieve all network interfaces.") @version(1, 0) -@temporary_cache(timeout_seconds=10) +@temporary_cache(timeout_seconds=1) def retrieve_interfaces() -> Any: """REST API endpoint to retrieve the all network interfaces.""" return manager.get_interfaces() From 59c9b868302ee07aede877d714d5f3f579ee573a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 9 Aug 2023 17:59:57 -0300 Subject: [PATCH 5/8] core: frontend: components: NetworkInterfacePriority: Fetch list afte applying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../frontend/src/components/app/NetworkInterfacePriorityMenu.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue index a99292165b..a12e6479bc 100644 --- a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -95,6 +95,7 @@ export default Vue.extend({ ) this.close() }) + await this.fetchAvailableInterfaces() }, async fetchAvailableInterfaces(): Promise { await back_axios({ From 222643e18ffb163a8418aebd31283ece57b778ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 10 Aug 2023 09:01:10 -0300 Subject: [PATCH 6/8] core: frontend: components: NetworkInterfacePriority: Disable controls if applying new priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../src/components/app/NetworkInterfacePriorityMenu.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue index a12e6479bc..979ec18dc9 100644 --- a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -76,6 +76,7 @@ export default Vue.extend({ return `${index}${['st', 'nd', 'rd'][((index + 90) % 100 - 10) % 10 - 1] || 'th'}` }, async setHighestInterface(): Promise { + this.is_loading = true const interface_priority = this.interfaces.map((inter) => ({ name: inter.name })) await back_axios({ method: 'post', @@ -96,6 +97,7 @@ export default Vue.extend({ this.close() }) await this.fetchAvailableInterfaces() + this.is_loading = false }, async fetchAvailableInterfaces(): Promise { await back_axios({ From 894652c853fdcd7dc6d49f60b40fcfd66e88391c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 10 Aug 2023 09:04:15 -0300 Subject: [PATCH 7/8] core: frontend: components: NetworkInterfacePriority: Add cancel button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../src/components/app/NetworkInterfacePriorityMenu.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue index 979ec18dc9..5b1383d16e 100644 --- a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -30,6 +30,13 @@ + + Cancel + + Date: Thu, 10 Aug 2023 09:48:39 -0300 Subject: [PATCH 8/8] core: frontend: components: NetworkInterfacePriority: Add text about system reboot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../src/components/app/NetworkInterfacePriorityMenu.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue index 5b1383d16e..f194b11c03 100644 --- a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -3,7 +3,7 @@ Move network interfaces over - to change network access priority + to change network access priority.
Applied changes require a system reboot.