From 22459047408e9d5f9ea2c528b43ee416b3a6a841 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 26 Jun 2023 09:52:30 -0600 Subject: [PATCH] feature: remove ssh references when getting MAC on bosminer. --- pyasic/miners/backends/bosminer.py | 41 ++++++++++--- pyasic/miners/miner_factory.py | 5 +- pyasic/web/bosminer.py | 95 +++++++++++++++++++++++++++--- 3 files changed, 124 insertions(+), 17 deletions(-) diff --git a/pyasic/miners/backends/bosminer.py b/pyasic/miners/backends/bosminer.py index 18a894d9..1ba2ca24 100644 --- a/pyasic/miners/backends/bosminer.py +++ b/pyasic/miners/backends/bosminer.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ - +import asyncio import logging from collections import namedtuple from typing import List, Optional, Tuple, Union @@ -29,7 +29,12 @@ from pyasic.miners.base import BaseMiner from pyasic.web.bosminer import BOSMinerWebAPI BOSMINER_DATA_LOC = { - "mac": {"cmd": "get_mac", "kwargs": {}}, + "mac": { + "cmd": "get_mac", + "kwargs": { + "web_net_conf": {"web": "/cgi-bin/luci/admin/network/iface_status/lan"} + }, + }, "model": {"cmd": "get_model", "kwargs": {}}, "api_ver": { "cmd": "get_api_ver", @@ -196,8 +201,8 @@ class BOSMiner(BaseMiner): result = None try: - conn = await self._get_ssh_connection() - except ConnectionError: + conn = await asyncio.wait_for(self._get_ssh_connection(), timeout=10) + except (ConnectionError, asyncio.TimeoutError): return None # open an ssh connection @@ -368,10 +373,30 @@ class BOSMiner(BaseMiner): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def get_mac(self) -> Optional[str]: - result = await self.send_ssh_command("cat /sys/class/net/eth0/address") - if result: - return result.upper().strip() + async def get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]: + if not web_net_conf: + try: + web_net_conf = await self.web.send_command( + "/cgi-bin/luci/admin/network/iface_status/lan" + ) + except APIError: + pass + + if isinstance(web_net_conf, dict): + if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys(): + web_net_conf = web_net_conf[ + "/cgi-bin/luci/admin/network/iface_status/lan" + ] + + if web_net_conf: + try: + return web_net_conf[0]["macaddr"] + except LookupError: + pass + # could use ssh, but its slow and buggy + # result = await self.send_ssh_command("cat /sys/class/net/eth0/address") + # if result: + # return result.upper().strip() async def get_model(self) -> Optional[str]: if self.model is not None: diff --git a/pyasic/miners/miner_factory.py b/pyasic/miners/miner_factory.py index 36b70f9b..4aaa1e79 100644 --- a/pyasic/miners/miner_factory.py +++ b/pyasic/miners/miner_factory.py @@ -625,7 +625,10 @@ class MinerFactory: data = await self._fix_api_data(data) - data = json.loads(data) + try: + data = json.loads(data) + except json.JSONDecodeError: + return {} return data diff --git a/pyasic/web/bosminer.py b/pyasic/web/bosminer.py index 4710e532..ef500f2a 100644 --- a/pyasic/web/bosminer.py +++ b/pyasic/web/bosminer.py @@ -18,6 +18,7 @@ from typing import Union import httpx +from pyasic import APIError from pyasic.settings import PyasicSettings from pyasic.web import BaseWebAPI @@ -27,6 +28,18 @@ class BOSMinerWebAPI(BaseWebAPI): super().__init__(ip) self.pwd = PyasicSettings().global_bosminer_password + async def send_command( + self, + command: Union[str, dict], + ignore_errors: bool = False, + allow_warning: bool = True, + **parameters: Union[str, int, bool], + ) -> dict: + if isinstance(command, str): + return await self.send_luci_command(command) + else: + return await self.send_gql_command(command) + def parse_command(self, graphql_command: Union[dict, set]) -> str: if isinstance(graphql_command, dict): data = [] @@ -40,12 +53,9 @@ class BOSMinerWebAPI(BaseWebAPI): data = graphql_command return "{" + ",".join(data) + "}" - async def send_command( + async def send_gql_command( self, command: dict, - ignore_errors: bool = False, - allow_warning: bool = True, - **parameters: Union[str, int, bool], ) -> dict: url = f"http://{self.ip}/graphql" query = self.parse_command(command) @@ -63,8 +73,29 @@ class BOSMinerWebAPI(BaseWebAPI): pass async def multicommand( - self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True - ) -> dict: + self, *commands: Union[dict, str], allow_warning: bool = True + ): + luci_commands = [] + gql_commands = [] + for cmd in commands: + if isinstance(cmd, dict): + gql_commands.append(cmd) + if isinstance(cmd, str): + luci_commands.append(cmd) + + luci_data = await self.luci_multicommand(*luci_commands) + gql_data = await self.gql_multicommand(*gql_commands) + + data = dict(**luci_data, **gql_data) + return data + + async def luci_multicommand(self, *commands: str) -> dict: + data = {} + for command in commands: + data[command] = await self.send_luci_command(command, ignore_errors=True) + return data + + async def gql_multicommand(self, *commands: dict) -> dict: def merge(*d: dict): ret = {} for i in d: @@ -85,8 +116,8 @@ class BOSMinerWebAPI(BaseWebAPI): # noinspection PyTypeChecker commands.remove({"bos": {"faultLight": None}}) command = merge(*commands) - data = await self.send_command(command) - except LookupError: + data = await self.send_gql_command(command) + except (LookupError, ValueError): pass if not data: data = {} @@ -105,3 +136,51 @@ class BOSMinerWebAPI(BaseWebAPI): + '"){__typename}}}' }, ) + + async def send_luci_command(self, path: str, ignore_errors: bool = False) -> dict: + try: + async with httpx.AsyncClient() as client: + await self.luci_auth(client) + data = await client.get(f"http://{self.ip}{path}") + if data.status_code == 200: + return data.json() + if ignore_errors: + return {} + raise APIError( + f"Web command failed: path={path}, code={data.status_code}" + ) + except (httpx.HTTPError, json.JSONDecodeError): + if ignore_errors: + return {} + raise APIError(f"Web command failed: path={path}") + + async def luci_auth(self, session: httpx.AsyncClient): + login = {"luci_username": self.username, "luci_password": self.pwd} + url = f"http://{self.ip}/cgi-bin/luci" + headers = { + "User-Agent": "BTC Tools v0.1", # only seems to respond if this user-agent is set + "Content-Type": "application/x-www-form-urlencoded", + } + d = await session.post(url, headers=headers, data=login) + + async def get_net_conf(self): + return await self.send_luci_command( + "/cgi-bin/luci/admin/network/iface_status/lan" + ) + + async def get_cfg_metadata(self): + return await self.send_luci_command("/cgi-bin/luci/admin/miner/cfg_metadata") + + async def get_cfg_data(self): + return await self.send_luci_command("/cgi-bin/luci/admin/miner/cfg_data") + + async def get_bos_info(self): + return await self.send_luci_command("/cgi-bin/luci/bos/info") + + async def get_overview(self): + return await self.send_luci_command( + "/cgi-bin/luci/admin/status/overview?status=1" + ) # needs status=1 or it fails + + async def get_api_status(self): + return await self.send_luci_command("/cgi-bin/luci/admin/miner/api_status")