From 4c3ddf9066c0a6c5719be9dd725dc5602d344c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Fri, 23 Jun 2023 15:08:43 -0300 Subject: [PATCH 01/12] core: services: cable_guy: Move from get_interfaces to get_ethernet_interfaces 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 | 10 +++++----- core/services/cable_guy/main.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index 15ec950507..f26737f000 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -72,7 +72,7 @@ def __init__(self, default_configs: List[EthernetInterface]) -> None: def save(self) -> None: """Save actual configuration""" try: - self.get_interfaces() + self.get_ethernet_interfaces() except Exception as exception: logger.error(f"Failed to fetch actual configuration, going to use the previous info: {exception}") @@ -89,7 +89,7 @@ def set_configuration(self, interface: EthernetInterface) -> None: Args: interface: EthernetInterface """ - interfaces = self.get_interfaces() + interfaces = self.get_ethernet_interfaces() logger.debug(f"Found following ethernet interfaces: {interfaces}.") valid_names = [interface.name for interface in interfaces] @@ -275,13 +275,13 @@ 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 def get_interface_by_name(self, name: str) -> EthernetInterface: - for interface in self.get_interfaces(): + for interface in self.get_ethernet_interfaces(): if interface.name == name: return interface raise ValueError(f"No interface with name '{name}' is present.") - def get_interfaces(self) -> List[EthernetInterface]: - """Get interfaces information + def get_ethernet_interfaces(self) -> List[EthernetInterface]: + """Get ethernet interfaces information Returns: List of EthernetInterface instances available diff --git a/core/services/cable_guy/main.py b/core/services/cable_guy/main.py index 23c3f56570..af5ce3d2c1 100755 --- a/core/services/cable_guy/main.py +++ b/core/services/cable_guy/main.py @@ -64,7 +64,7 @@ @temporary_cache(timeout_seconds=10) def retrieve_interfaces() -> Any: """REST API endpoint to retrieve the configured ethernet interfaces.""" - return manager.get_interfaces() + return manager.get_ethernet_interfaces() @app.post("/ethernet", response_model=EthernetInterface, summary="Configure a ethernet interface.") From 9a772c24cbbacf94777aaa47f44150445c23e716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Fri, 23 Jun 2023 15:25:00 -0300 Subject: [PATCH 02/12] core: services: cable_guy: Move from EthernetInterface to NetworkInterface 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 | 26 +++++++++++++------------- core/services/cable_guy/main.py | 13 +++++++------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index f26737f000..91c134b419 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -30,7 +30,7 @@ class InterfaceInfo(BaseModel): number_of_disconnections: int -class EthernetInterface(BaseModel): +class NetworkInterface(BaseModel): name: str addresses: List[InterfaceAddress] info: Optional[InterfaceInfo] @@ -44,9 +44,9 @@ class EthernetManager: # IP abstraction interface ipr = IPRoute() - result: List[EthernetInterface] = [] + result: List[NetworkInterface] = [] - def __init__(self, default_configs: List[EthernetInterface]) -> None: + def __init__(self, default_configs: List[NetworkInterface]) -> None: self.settings = settings.Settings() self._dhcp_servers: List[DHCPServerManager] = [] @@ -65,7 +65,7 @@ def __init__(self, default_configs: List[EthernetInterface]) -> None: for item in self.settings.root["content"]: logger.info(f"Loading following configuration: {item}.") try: - self.set_configuration(EthernetInterface(**item)) + self.set_configuration(NetworkInterface(**item)) except Exception as error: logger.error(f"Failed loading saved configuration. {error}") @@ -83,11 +83,11 @@ def save(self) -> None: result = [interface.dict(exclude={"info"}) for interface in self.result] self.settings.save(result) - def set_configuration(self, interface: EthernetInterface) -> None: + def set_configuration(self, interface: NetworkInterface) -> None: """Modify hardware based in the configuration Args: - interface: EthernetInterface + interface: NetworkInterface """ interfaces = self.get_ethernet_interfaces() logger.debug(f"Found following ethernet interfaces: {interfaces}.") @@ -148,11 +148,11 @@ def is_valid_interface_name(self, interface_name: str) -> bool: return True - def validate_interface_data(self, interface: EthernetInterface) -> bool: + def validate_interface_data(self, interface: NetworkInterface) -> bool: """Check if interface configuration is valid Args: - interface: EthernetInterface instance + interface: NetworkInterface instance Returns: bool: True if valid, False if not @@ -160,7 +160,7 @@ def validate_interface_data(self, interface: EthernetInterface) -> bool: return self.is_valid_interface_name(interface.name) @staticmethod - def _is_server_address_present(interface: EthernetInterface) -> bool: + def _is_server_address_present(interface: NetworkInterface) -> bool: return any(address.mode == AddressMode.Server for address in interface.addresses) @staticmethod @@ -274,17 +274,17 @@ def remove_ip(self, interface_name: str, ip_address: str) -> None: except Exception as error: raise RuntimeError(f"Cannot delete IP '{ip_address}' from interface {interface_name}.") from error - def get_interface_by_name(self, name: str) -> EthernetInterface: + def get_interface_by_name(self, name: str) -> NetworkInterface: for interface in self.get_ethernet_interfaces(): if interface.name == name: return interface raise ValueError(f"No interface with name '{name}' is present.") - def get_ethernet_interfaces(self) -> List[EthernetInterface]: + def get_ethernet_interfaces(self) -> List[NetworkInterface]: """Get ethernet interfaces information Returns: - List of EthernetInterface instances available + List of NetworkInterface instances available """ result = [] for interface, addresses in psutil.net_if_addrs().items(): @@ -316,7 +316,7 @@ def get_ethernet_interfaces(self) -> List[EthernetInterface]: valid_addresses.append(InterfaceAddress(ip=ip, mode=mode)) info = self.get_interface_info(interface) - interface_data = EthernetInterface(name=interface, addresses=valid_addresses, info=info) + interface_data = NetworkInterface(name=interface, addresses=valid_addresses, info=info) # Check if it's valid and add to the result if self.validate_interface_data(interface_data): result += [interface_data] diff --git a/core/services/cable_guy/main.py b/core/services/cable_guy/main.py index af5ce3d2c1..99c3fe2693 100755 --- a/core/services/cable_guy/main.py +++ b/core/services/cable_guy/main.py @@ -18,9 +18,10 @@ from api.manager import ( AddressMode, - EthernetInterface, EthernetManager, InterfaceAddress, + NetworkInterface, + NetworkInterfaceMetricApi, ) SERVICE_NAME = "cable-guy" @@ -39,8 +40,8 @@ if args.default_config == "bluerov2": default_configs = [ - EthernetInterface(name="eth0", addresses=[InterfaceAddress(ip="192.168.2.2", mode=AddressMode.Unmanaged)]), - EthernetInterface(name="usb0", addresses=[InterfaceAddress(ip="192.168.3.1", mode=AddressMode.Server)]), + NetworkInterface(name="eth0", addresses=[InterfaceAddress(ip="192.168.2.2", mode=AddressMode.Unmanaged)]), + NetworkInterface(name="usb0", addresses=[InterfaceAddress(ip="192.168.3.1", mode=AddressMode.Server)]), ] manager = EthernetManager(default_configs) @@ -59,7 +60,7 @@ app.router.route_class = GenericErrorHandlingRoute -@app.get("/ethernet", response_model=List[EthernetInterface], summary="Retrieve ethernet interfaces.") +@app.get("/ethernet", response_model=List[NetworkInterface], summary="Retrieve ethernet interfaces.") @version(1, 0) @temporary_cache(timeout_seconds=10) def retrieve_interfaces() -> Any: @@ -67,9 +68,9 @@ def retrieve_interfaces() -> Any: return manager.get_ethernet_interfaces() -@app.post("/ethernet", response_model=EthernetInterface, summary="Configure a ethernet interface.") +@app.post("/ethernet", response_model=NetworkInterface, summary="Configure a ethernet interface.") @version(1, 0) -def configure_interface(interface: EthernetInterface = Body(...)) -> Any: +def configure_interface(interface: NetworkInterface = Body(...)) -> Any: """REST API endpoint to configure a new ethernet interface or modify an existing one.""" manager.set_configuration(interface) manager.save() From cde8f4cf8d8627da1dd5e56ba80f7129b420e0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 28 Jun 2023 15:31:56 -0300 Subject: [PATCH 03/12] core: frontend: components: Add NetowkrInterfacePriorityMenu and InternetTrayMenu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../src/components/app/InternetTrayMenu.vue | 64 ++++++++++ .../app/NetworkInterfacePriorityMenu.vue | 116 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 core/frontend/src/components/app/InternetTrayMenu.vue create mode 100644 core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue diff --git a/core/frontend/src/components/app/InternetTrayMenu.vue b/core/frontend/src/components/app/InternetTrayMenu.vue new file mode 100644 index 0000000000..7cb2bcded9 --- /dev/null +++ b/core/frontend/src/components/app/InternetTrayMenu.vue @@ -0,0 +1,64 @@ + + + diff --git a/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue new file mode 100644 index 0000000000..5b5ad71d10 --- /dev/null +++ b/core/frontend/src/components/app/NetworkInterfacePriorityMenu.vue @@ -0,0 +1,116 @@ + + + From 116540ca82e301c6c2e52d22c61dd386d3004a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 28 Jun 2023 15:32:15 -0300 Subject: [PATCH 04/12] core: frontend: App: Fix draggable name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/App.vue b/core/frontend/src/App.vue index 725db17b08..445e1d640d 100644 --- a/core/frontend/src/App.vue +++ b/core/frontend/src/App.vue @@ -36,7 +36,7 @@ color="white" @click="drawer = true" /> - + Date: Wed, 28 Jun 2023 15:32:45 -0300 Subject: [PATCH 05/12] core: frontend: App: Add InternetTrayMenu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/App.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/frontend/src/App.vue b/core/frontend/src/App.vue index 445e1d640d..bf7ed0e6ed 100644 --- a/core/frontend/src/App.vue +++ b/core/frontend/src/App.vue @@ -53,6 +53,7 @@ + @@ -326,6 +327,7 @@ import updateTime from '@/utils/update_time' import Alerter from './components/app/Alerter.vue' import BackendStatusChecker from './components/app/BackendStatusChecker.vue' +import InternetTrayMenu from './components/app/InternetTrayMenu.vue' import NewVersionNotificator from './components/app/NewVersionNotificator.vue' import PiradeModeTrayMenu from './components/app/PirateModeTrayMenu.vue' import PowerMenu from './components/app/PowerMenu.vue' @@ -352,6 +354,7 @@ export default Vue.extend({ components: { 'beacon-tray-menu': BeaconTrayMenu, + 'internet-tray-menu': InternetTrayMenu, 'notification-tray-button': NotificationTrayButton, 'pirate-mode-tray-menu': PiradeModeTrayMenu, 'theme-tray-menu': ThemeTrayMenu, From 1c0d87fe27a936d2766c1e864782072338baf545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 28 Jun 2023 15:33:35 -0300 Subject: [PATCH 06/12] core: services: cable_guy: Add function to get all network interfaces 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 | 32 ++++++++++++++++++-------- core/services/cable_guy/main.py | 10 +++++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index 91c134b419..b52569bab7 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -125,18 +125,20 @@ def _get_wifi_interfaces(self) -> List[str]: result += [value] return result - def is_valid_interface_name(self, interface_name: str) -> bool: + def is_valid_interface_name(self, interface_name: str, filter_wifi: bool = False) -> bool: """Check if an interface name is valid Args: interface_name (str): Network interface name + filter_wifi (boolean, optional): Enable wifi interface filtering Returns: bool: True if valid, False if not """ blacklist = ["lo", "ham.*", "docker.*", "veth.*"] - wifi_interfaces = self._get_wifi_interfaces() - blacklist += wifi_interfaces + if filter_wifi: + wifi_interfaces = self._get_wifi_interfaces() + blacklist += wifi_interfaces if not interface_name: logger.error("Interface name cannot be blank or null.") @@ -148,16 +150,17 @@ def is_valid_interface_name(self, interface_name: str) -> bool: return True - def validate_interface_data(self, interface: NetworkInterface) -> bool: + def validate_interface_data(self, interface: NetworkInterface, filter_wifi: bool = False) -> bool: """Check if interface configuration is valid Args: interface: NetworkInterface instance + filter_wifi (boolean, optional): Enable wifi interface filtering Returns: bool: True if valid, False if not """ - return self.is_valid_interface_name(interface.name) + return self.is_valid_interface_name(interface.name, filter_wifi) @staticmethod def _is_server_address_present(interface: NetworkInterface) -> bool: @@ -280,8 +283,11 @@ def get_interface_by_name(self, name: str) -> NetworkInterface: return interface raise ValueError(f"No interface with name '{name}' is present.") - def get_ethernet_interfaces(self) -> List[NetworkInterface]: - """Get ethernet interfaces information + def get_interfaces(self, filter_wifi: bool = False) -> List[NetworkInterface]: + """Get interfaces information + + Args: + filter_wifi (boolean, optional): Enable wifi interface filtering Returns: List of NetworkInterface instances available @@ -291,7 +297,7 @@ def get_ethernet_interfaces(self) -> List[NetworkInterface]: # We don't care about virtual ethernet interfaces ## Virtual interfaces are created by programs such as docker ## and they are an abstraction of real interfaces, the ones that we want to configure. - if not self.is_valid_interface_name(interface): + if not self.is_valid_interface_name(interface, filter_wifi): continue valid_addresses = [] @@ -318,12 +324,20 @@ def get_ethernet_interfaces(self) -> List[NetworkInterface]: info = self.get_interface_info(interface) interface_data = NetworkInterface(name=interface, addresses=valid_addresses, info=info) # Check if it's valid and add to the result - if self.validate_interface_data(interface_data): + if self.validate_interface_data(interface_data, filter_wifi): result += [interface_data] self.result = result return result + def get_ethernet_interfaces(self) -> List[NetworkInterface]: + """Get ethernet interfaces information + + Returns: + List of NetworkInterface instances available + """ + return self.get_interfaces(filter_wifi=True) + def get_interface_ndb(self, interface_name: str) -> Any: """Get interface NDB information for interface diff --git a/core/services/cable_guy/main.py b/core/services/cable_guy/main.py index 99c3fe2693..2546fcaa32 100755 --- a/core/services/cable_guy/main.py +++ b/core/services/cable_guy/main.py @@ -63,7 +63,7 @@ @app.get("/ethernet", response_model=List[NetworkInterface], summary="Retrieve ethernet interfaces.") @version(1, 0) @temporary_cache(timeout_seconds=10) -def retrieve_interfaces() -> Any: +def retrieve_ethernet_interfaces() -> Any: """REST API endpoint to retrieve the configured ethernet interfaces.""" return manager.get_ethernet_interfaces() @@ -77,6 +77,14 @@ def configure_interface(interface: NetworkInterface = Body(...)) -> Any: return interface +@app.get("/interfaces", response_model=List[NetworkInterface], summary="Retrieve all network interfaces.") +@version(1, 0) +@temporary_cache(timeout_seconds=10) +def retrieve_interfaces() -> Any: + """REST API endpoint to retrieve the all network interfaces.""" + return manager.get_interfaces() + + @app.post("/address", summary="Add IP address to interface.") @version(1, 0) def add_address(interface_name: str, ip_address: str) -> Any: From 9563572685f39edd5c0164263a25c46d9fb8953d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 28 Jun 2023 15:35:20 -0300 Subject: [PATCH 07/12] core: frontend: store: Add helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/store/helper.ts | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 core/frontend/src/store/helper.ts diff --git a/core/frontend/src/store/helper.ts b/core/frontend/src/store/helper.ts new file mode 100644 index 0000000000..eee91ea676 --- /dev/null +++ b/core/frontend/src/store/helper.ts @@ -0,0 +1,62 @@ +import { + Action, + getModule, Module, Mutation, VuexModule, +} from 'vuex-module-decorators' + +import Notifier from '@/libs/notifier' +import store from '@/store' +import { helper_service } from '@/types/frontend_services' +import back_axios from '@/utils/api' +import { callPeriodically } from '@/utils/helper_functions' + +const notifier = new Notifier(helper_service) + +type CheckSiteStatus = { + site: string; + online: boolean; + error: string | null; +}; + +type SiteStatus = Record + +@Module({ + dynamic: true, + store, + name: 'helper', +}) + +class PingStore extends VuexModule { + API_URL = '/helper/latest' + + has_internet = false + + @Mutation + setHasInternet(has_internet: boolean): void { + this.has_internet = has_internet + } + + @Action + async checkInternetAccess(): Promise { + back_axios({ + method: 'get', + url: `${this.API_URL}/check_internet_access`, + timeout: 10000, + }) + .then((response) => { + const has_internet = !Object.values(response.data as SiteStatus) + .filter((item) => item.online) + .isEmpty() + + this.setHasInternet(has_internet) + }) + .catch((error) => { + notifier.pushBackError('INTERNET_CHECK_FAIL', error) + }) + } +} + +export { PingStore } + +const ping: PingStore = getModule(PingStore) +callPeriodically(ping.checkInternetAccess, 20000) +export default ping From 5154609efe2167f71c5e023111eed3cffc7f3660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Wed, 28 Jun 2023 15:35:35 -0300 Subject: [PATCH 08/12] core: frontend: types: frontend_services: Add helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/types/frontend_services.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/frontend/src/types/frontend_services.ts b/core/frontend/src/types/frontend_services.ts index 90491758fb..2f298fce96 100644 --- a/core/frontend/src/types/frontend_services.ts +++ b/core/frontend/src/types/frontend_services.ts @@ -132,3 +132,10 @@ export const bag_of_holders_service: Service = { company: 'Blue Robotics', version: '0.1.0', } + +export const helper_service: Service = { + name: 'Helper service', + description: 'Simple helper service for the frontend and simple features', + company: 'Blue Robotics', + version: '0.1.0', +} From b474c43fb213b7d3a5657e17bab28ae2f7a23207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 29 Jun 2023 13:49:15 -0300 Subject: [PATCH 09/12] core: services: cable_guy: Update inteface with metric/priority information 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 | 75 +++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index b52569bab7..030233f95b 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -2,9 +2,10 @@ import time from enum import Enum from socket import AddressFamily -from typing import Any, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import psutil +from commonwealth.utils.decorators import temporary_cache from commonwealth.utils.DHCPServerManager import Dnsmasq as DHCPServerManager from loguru import logger from pydantic import BaseModel @@ -28,6 +29,7 @@ class InterfaceAddress(BaseModel): class InterfaceInfo(BaseModel): connected: bool number_of_disconnections: int + priority: int class NetworkInterface(BaseModel): @@ -36,6 +38,17 @@ class NetworkInterface(BaseModel): info: Optional[InterfaceInfo] +class NetworkInterfaceMetric(BaseModel): + name: str + index: int + priority: int + + +class NetworkInterfaceMetricApi(BaseModel): + name: str + priority: Optional[int] + + class EthernetManager: # RTNL interface ndb = NDB(log="on") @@ -349,6 +362,58 @@ def get_interface_ndb(self, interface_name: str) -> Any: """ return self.ndb.interfaces.dump().filter(ifname=interface_name)[0] + @temporary_cache(timeout_seconds=5) + def get_interfaces_priority(self) -> List[NetworkInterfaceMetric]: + """Get the priority metrics for all network interfaces. + + Returns: + List[NetworkInterfaceMetric]: A list of priority metrics for each interface. + """ + + interfaces = self.ipr.get_links() + # I hope that you are not here to move this code to IPv6. + # If that is the case, you'll need to figure out a way to handle + # priorities between interfaces, between IP categories. + # 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"] + + return [ + NetworkInterfaceMetric(index=index, name=name, priority=metric_dict.get(index) or 0) + for index, name in name_dict.items() + ] + + def get_interface_priority(self, interface_name: str) -> Optional[NetworkInterfaceMetric]: + """Get the priority metric for a network interface. + + Args: + interface_name (str): The name of the network interface. + + Returns: + Optional[NetworkInterfaceMetric]: The priority metric for the interface, or None if no metric found. + """ + metric: NetworkInterfaceMetric + for metric in self.get_interfaces_priority(): + if interface_name == metric.name: + return metric + + return None + def get_interface_info(self, interface_name: str) -> InterfaceInfo: """Get interface info field @@ -358,8 +423,14 @@ def get_interface_info(self, interface_name: str) -> InterfaceInfo: Returns: InterfaceInfo object """ + metric = self.get_interface_priority(interface_name) + priority = metric.priority if metric else 0 interface = self.get_interface_ndb(interface_name) - return InterfaceInfo(connected=interface.carrier != 0, number_of_disconnections=interface.carrier_down_count) + return InterfaceInfo( + connected=interface.carrier != 0, + number_of_disconnections=interface.carrier_down_count, + priority=priority, + ) def _is_ip_on_interface(self, interface_name: str, ip_address: str) -> bool: interface = self.get_interface_by_name(interface_name) From df05dffbb0bdf50dcf3cc97a23cc700136127301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 29 Jun 2023 14:53:44 -0300 Subject: [PATCH 10/12] core: frontend: types: ethernet: Add priority in InterfaceInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/types/ethernet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/frontend/src/types/ethernet.ts b/core/frontend/src/types/ethernet.ts index 1479c16511..8aa5ecda8e 100644 --- a/core/frontend/src/types/ethernet.ts +++ b/core/frontend/src/types/ethernet.ts @@ -12,6 +12,7 @@ export interface InterfaceAddress { export interface InterfaceInfo { connected: boolean, number_of_disconnections: number, + priority: number, } export interface EthernetInterface { From 70accf96b5c278343090aa849dc6c1f910ae637c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 29 Jun 2023 15:05:04 -0300 Subject: [PATCH 11/12] core: services: cable_guy: Add endpoint to set interface to highest priority 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 | 66 ++++++++++++++++++++++++++ core/services/cable_guy/main.py | 7 +++ 2 files changed, 73 insertions(+) diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index 030233f95b..5f0bbe2d22 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -1,4 +1,5 @@ import re +import subprocess import time from enum import Enum from socket import AddressFamily @@ -414,6 +415,71 @@ def get_interface_priority(self, interface_name: str) -> Optional[NetworkInterfa return None + def set_interfaces_priority(self, interfaces: List[NetworkInterfaceMetricApi]) -> None: + """Set interfaces priority. + + 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 + + Args: + name (str): Interface name + priority (int): Priority + """ + + # Change the priority for all routes that are using this interface + # There is no pretty way of doing it with pyroute2 + result = subprocess.run( + ["ifmetric", name, str(priority)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to change network priority {name}") + def get_interface_info(self, interface_name: str) -> InterfaceInfo: """Get interface info field diff --git a/core/services/cable_guy/main.py b/core/services/cable_guy/main.py index 2546fcaa32..862ffb8e2b 100755 --- a/core/services/cable_guy/main.py +++ b/core/services/cable_guy/main.py @@ -85,6 +85,13 @@ def retrieve_interfaces() -> Any: return manager.get_interfaces() +@app.post("/set_interfaces_priority", summary="Set interface priority") +@version(1, 0) +def set_interfaces_priority(interfaces: List[NetworkInterfaceMetricApi]) -> Any: + """REST API endpoint to set the interface priority.""" + return manager.set_interfaces_priority(interfaces) + + @app.post("/address", summary="Add IP address to interface.") @version(1, 0) def add_address(interface_name: str, ip_address: str) -> Any: From e38cc512e4a9aefeba74240b7291ece1a3d4c55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 29 Jun 2023 16:54:19 -0300 Subject: [PATCH 12/12] core: tools: Add cable_guy bootstrap with ifmetric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/tools/cable_guy/bootstrap.sh | 9 +++++++++ core/tools/install-system-tools.sh | 1 + 2 files changed, 10 insertions(+) create mode 100755 core/tools/cable_guy/bootstrap.sh diff --git a/core/tools/cable_guy/bootstrap.sh b/core/tools/cable_guy/bootstrap.sh new file mode 100755 index 0000000000..1d16b7f2c3 --- /dev/null +++ b/core/tools/cable_guy/bootstrap.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Exit if something goes wrong +set -e + +# Hostapd turns a network wireless interface into a wifi hotspot +echo "Installing ifmetric." +apt update +apt install -y --no-install-recommends ifmetric diff --git a/core/tools/install-system-tools.sh b/core/tools/install-system-tools.sh index daa1d8bbab..d2c94436a9 100755 --- a/core/tools/install-system-tools.sh +++ b/core/tools/install-system-tools.sh @@ -18,5 +18,6 @@ parallel --halt now,fail=1 '/home/pi/tools/{}/bootstrap.sh' ::: "${TOOLS[@]}" # Tools that uses apt to do the installation # APT is terrible like pip and don't know how to handle parallel installation +/home/pi/tools/cable_guy/bootstrap.sh /home/pi/tools/dnsmasq/bootstrap.sh /home/pi/tools/nginx/bootstrap.sh \ No newline at end of file