From cdc6c898ae705548dedf9eaf0872f6b72dcf596e Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Wed, 12 Jan 2022 09:04:15 -0700 Subject: [PATCH] reformatted, added a bunch of comments to improve readability, and added a whatsminer admin password in settings --- API/btminer.py | 10 +++- miners/antminer/X17/bmminer.py | 2 +- miners/miner_factory.py | 93 +++++++++++++++++++++++++++++++++- misc/bos.py | 25 +++++++++ network/__init__.py | 59 +++++++++++++++++++-- settings/__init__.py | 4 ++ settings/settings.toml | 4 +- 7 files changed, 187 insertions(+), 10 deletions(-) diff --git a/API/btminer.py b/API/btminer.py index de878a81..e0162f6f 100644 --- a/API/btminer.py +++ b/API/btminer.py @@ -1,4 +1,5 @@ from API import BaseMinerAPI, APIError +from settings import WHATSMINER_PWD from passlib.handlers.md5_crypt import md5_crypt import asyncio @@ -16,7 +17,9 @@ import base64 # admin with this tool, but they must be changed to # something else and set back to admin with this or # the privileged API will not work using admin as -# the password. +# the password. If you change the password, you can +# pass that to the this class as pwd, or added as +# whatsminer_pwd in the settings.toml file. def _crypt(word: str, salt: str) -> str: @@ -82,7 +85,10 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes: class BTMinerAPI(BaseMinerAPI): def __init__(self, ip, port=4028, pwd: str = "admin"): super().__init__(ip, port) - self.admin_pwd = pwd + if pwd: + self.admin_pwd = pwd + else: + self.admin_pwd = WHATSMINER_PWD self.current_token = None async def send_command(self, command: str | bytes, **kwargs) -> dict: diff --git a/miners/antminer/X17/bmminer.py b/miners/antminer/X17/bmminer.py index 374f7584..6eaecbf8 100644 --- a/miners/antminer/X17/bmminer.py +++ b/miners/antminer/X17/bmminer.py @@ -6,4 +6,4 @@ class BMMinerX17(BMMiner): super().__init__(ip) def __repr__(self) -> str: - return f"CGMinerX17: {str(self.ip)}" + return f"BMMinerX17: {str(self.ip)}" diff --git a/miners/miner_factory.py b/miners/miner_factory.py index 87c25302..690ee71a 100644 --- a/miners/miner_factory.py +++ b/miners/miner_factory.py @@ -45,11 +45,16 @@ class MinerFactory: Parameters: ips: a list of ip addresses to get miners for. """ + # get the event loop loop = asyncio.get_event_loop() + # create a list of tasks scan_tasks = [] + # for each miner IP that was passed in, add a task to get its class for miner in ips: scan_tasks.append(loop.create_task(self.get_miner(miner))) + # asynchronously run the tasks and return them as they complete scanned = asyncio.as_completed(scan_tasks) + # loop through and yield the miners as they complete for miner in scanned: yield await miner @@ -58,40 +63,69 @@ class MinerFactory: # check if the miner already exists in cache if ip in self.miners: return self.miners[ip] + # if everything fails, the miner is already set to unknown miner = UnknownMiner(str(ip)) api = None + model = None + + # try to get the API multiple times based on retries for i in range(GET_VERSION_RETRIES): + # get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None api = await self._get_api_type(ip) + # if we find the API type, dont need to loop anymore if api: break - model = None + + # try to get the model multiple times based on retries for i in range(GET_VERSION_RETRIES): + # get the model, should return some miner model type, e.g. Antminer S9 model = await self._get_miner_model(ip) + # if we find the model type, dont need to loop anymore if model: break + + # make sure we have model information if model: + + # check if the miner is an Antminer if "Antminer" in model: + + # S9 logic if "Antminer S9" in model: + + # handle the different API types if "BOSMiner" in api: miner = BOSMinerS9(str(ip)) elif "CGMiner" in api: miner = CGMinerS9(str(ip)) elif "BMMiner" in api: miner = BMMinerS9(str(ip)) + + # X17 model logic elif "17" in model: + + # handle the different API types if "BOSMiner" in api: miner = BOSMinerX17(str(ip)) elif "CGMiner" in api: miner = CGMinerX17(str(ip)) elif "BMMiner" in api: miner = BMMinerX17(str(ip)) + + # X19 logic elif "19" in model: + + # handle the different API types if "CGMiner" in api: miner = CGMinerX19(str(ip)) elif "BMMiner" in api: miner = BMMinerX19(str(ip)) + + # Avalonminer V8 elif "avalon" in model: miner = CGMinerAvalon(str(ip)) + + # Whatsminers elif "M20" in model: miner = BTMinerM20(str(ip)) elif "M21" in model: @@ -102,7 +136,11 @@ class MinerFactory: miner = BTMinerM31(str(ip)) elif "M32" in model: miner = BTMinerM32(str(ip)) + + # if we cant find a model, check if we found the API else: + + # return the miner base class with some API if we found it if api: if "BOSMiner" in api: miner = BOSMiner(str(ip)) @@ -110,37 +148,69 @@ class MinerFactory: miner = CGMiner(str(ip)) elif "BMMiner" in api: miner = BMMiner(str(ip)) + + # save the miner to the cache at its IP self.miners[ip] = miner + + # return the miner return miner def clear_cached_miners(self): """Clear the miner factory cache.""" + # empty out self.miners self.miners = {} - async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> dict or None: + async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> str or None: + # instantiate model as being nothing if getting it fails model = None + + # try block in case of APIError or OSError 121 (Semaphore timeout) try: + + # send the devdetails command to the miner (will fail with no boards/devices) data = await self._send_api_command(str(ip), "devdetails") + + # sometimes data is b'', check for that if data: + # status check, make sure the command succeeded if data.get("STATUS"): if not isinstance(data["STATUS"], str): + # if status is E, its an error if data["STATUS"][0].get("STATUS") not in ["I", "S"]: + + # try an alternate method if devdetails fails data = await self._send_api_command(str(ip), "version") + + # make sure we have data if data: + # check the keys are there to get the version if data.get("VERSION"): if data["VERSION"][0].get("Type"): + # save the model to be returned later model = data["VERSION"][0]["Type"] else: + # make sure devdetails actually contains data, if its empty, there are no devices if "DEVDETAILS" in data.keys() and not data["DEVDETAILS"] == []: + + # check for model, for most miners if not data["DEVDETAILS"][0]["Model"] == "": + # model of most miners model = data["DEVDETAILS"][0]["Model"] + + # if model fails, try driver else: + # some avalonminers have model in driver model = data["DEVDETAILS"][0]["Driver"] else: + # if all that fails, try just version data = await self._send_api_command(str(ip), "version") model = data["VERSION"][0]["Type"] + + # if we have a model, return it if model: return model + + # if there are errors, we just return None except APIError as e: return None except OSError as e: @@ -210,22 +280,41 @@ class MinerFactory: async def _get_api_type(self, ip: ipaddress.ip_address or str) -> dict or None: """Get data on the version of the miner to return the right miner.""" + # instantiate API as None in case something fails api = None + + # try block to handle OSError 121 (Semaphore timeout) try: + # try the version command,works on most miners data = await self._send_api_command(str(ip), "version") + + # if we got data back, try to parse it if data: + # make sure the command succeeded if data.get("STATUS") and not data.get("STATUS") == "E": if data["STATUS"][0].get("STATUS") in ["I", "S"]: + + # check if there are any BMMiner strings in any of the dict keys if any("BMMiner" in string for string in data["VERSION"][0].keys()): api = "BMMiner" + + # check if there are any CGMiner strings in any of the dict keys elif any("CGMiner" in string for string in data["VERSION"][0].keys()): api = "CGMiner" + + # check if there are any BOSMiner strings in any of the dict keys elif any("BOSminer" in string for string in data["VERSION"][0].keys()): api = "BOSMiner" + + # if all that fails, check the Description to see if it is a whatsminer elif data.get("Description") and "whatsminer" in data.get("Description"): api = "BTMiner" + + # return the API if we found it if api: return api + + # if there are errors, return None except OSError as e: if e.winerror == 121: return None diff --git a/misc/bos.py b/misc/bos.py index 584f956b..dd889b51 100644 --- a/misc/bos.py +++ b/misc/bos.py @@ -4,30 +4,55 @@ from miners.bosminer import BOSMiner async def get_bos_bad_tuners(): + # create a miner network miner_network = MinerNetwork("192.168.1.0") + + # scan for miners miners = await miner_network.scan_network_for_miners() + + # create an empty list of tasks tuner_tasks = [] + + # loop checks if the miner is a BOSMiner for miner in miners: # can only do this if its a subclass of BOSMiner if BOSMiner in type(miner).__bases__: tuner_tasks.append(_get_tuner_status(miner)) + + # run all the tuner status commands tuner_status = await asyncio.gather(*tuner_tasks) + + # create a list of all miners with bad board tuner status' bad_tuner_miners = [] for item in tuner_status: + # loop through and get each miners' bad board count bad_boards = [] for board in item["tuner_status"]: + # if its not stable or still testing, its bad if board["status"] not in ["Stable", "Testing performance profile"]: + # remove the part about the board refusing to start bad_boards.append({"board": board["board"], "error": board["status"].replace("Hashchain refused to start: ", "")}) + + # if this miner has bad boards, add it to the list of bad board miners if len(bad_boards) > 0: bad_tuner_miners.append({"ip": item["ip"], "boards": bad_boards}) + + # return the list of bad board miners return bad_tuner_miners async def _get_tuner_status(miner): + # run the tunerstatus command, since the miner will always be BOSMiner tuner_status = await miner.api.tunerstatus() + + # create a list to add the tuner data to tuner_data = [] + + # if we have data, loop through to get the hashchain status if tuner_status: for board in tuner_status["TUNERSTATUS"][0]["TunerChainStatus"]: tuner_data.append({"board": board["HashchainIndex"], "status": board["Status"]}) + + # return the data along with the IP or later tracking return {"ip": str(miner.ip), "tuner_status": tuner_data} diff --git a/network/__init__.py b/network/__init__.py index 1ea1d1b3..425a55c6 100644 --- a/network/__init__.py +++ b/network/__init__.py @@ -18,40 +18,75 @@ class MinerNetwork: def get_network(self) -> ipaddress.ip_network: """Get the network using the information passed to the MinerNetwork or from cache.""" + # if we have a network cached already, use that if self.network: return self.network + + # if there is no IP address passed, default to 192.168.1.0 if not self.ip_addr: default_gateway = "192.168.1.0" + # if we do have an IP address passed, use that else: default_gateway = self.ip_addr - if self.mask: - subnet_mask = str(self.mask) - else: + + # if there is no subnet mask passed, default to /24 + if not self.mask: subnet_mask = "24" - return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False) + # if we do have a mask passed, use that + else: + subnet_mask = str(self.mask) + + # save the network and return it + self.network = ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False) + return self.network async def scan_network_for_miners(self) -> None or list: """Scan the network for miners, and return found miners as a list.""" + # get the network local_network = self.get_network() print(f"Scanning {local_network} for miners...") + + # create a list of tasks and miner IPs scan_tasks = [] miner_ips = [] + + # for each IP in the network for host in local_network.hosts(): + + # make sure we don't exceed the allowed async tasks if len(scan_tasks) < SCAN_THREADS: + # add the task to the list scan_tasks.append(self.ping_miner(host)) else: + # run the scan tasks miner_ips_scan = await asyncio.gather(*scan_tasks) + # add scanned miners to the list of found miners miner_ips.extend(miner_ips_scan) + # empty the task list scan_tasks = [] + # do a final scan to empty out the list miner_ips_scan = await asyncio.gather(*scan_tasks) miner_ips.extend(miner_ips_scan) + + # remove all None from the miner list miner_ips = list(filter(None, miner_ips)) print(f"Found {len(miner_ips)} connected miners...") + + # create a list of tasks to get miners create_miners_tasks = [] + + # clear cached miners self.miner_factory.clear_cached_miners() + + # try to get each miner found for miner_ip in miner_ips: + # append to the list of tasks create_miners_tasks.append(self.miner_factory.get_miner(miner_ip)) + + # get all miners in the list miners = await asyncio.gather(*create_miners_tasks) + + # return the miner objects return miners async def scan_network_generator(self): @@ -60,16 +95,32 @@ class MinerNetwork: Returns an asynchronous generator containing found miners. """ + # get the current event loop loop = asyncio.get_event_loop() + + # get the network local_network = self.get_network() + + # create a list of scan tasks scan_tasks = [] + + # for each ip on the network, loop through and scan it for host in local_network.hosts(): + # make sure we don't exceed the allowed async tasks if len(scan_tasks) >= SCAN_THREADS: + # scanned is a loopable list of awaitables scanned = asyncio.as_completed(scan_tasks) + # when we scan, empty the scan tasks scan_tasks = [] + + # yield miners as they are scanned for miner in scanned: yield await miner + + # add the ping to the list of tasks if we dont scan scan_tasks.append(loop.create_task(self.ping_miner(host))) + + # do one last scan at the end to close out the list scanned = asyncio.as_completed(scan_tasks) for miner in scanned: yield await miner diff --git a/settings/__init__.py b/settings/__init__.py index 5642f47e..b836245f 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -12,6 +12,8 @@ try: CFG_UTIL_CONFIG_THREADS: int = settings["config_threads"] MINER_FACTORY_GET_VERSION_RETRIES: int = settings["get_version_retries"] + + WHATSMINER_PWD: str = settings["whatsminer_pwd"] except: NETWORK_PING_RETRIES: int = 3 NETWORK_PING_TIMEOUT: int = 5 @@ -21,3 +23,5 @@ except: CFG_UTIL_CONFIG_THREADS: int = 300 MINER_FACTORY_GET_VERSION_RETRIES: int = 3 + + WHATSMINER_PWD = "admin" diff --git a/settings/settings.toml b/settings/settings.toml index 79e7be6f..f98913a7 100644 --- a/settings/settings.toml +++ b/settings/settings.toml @@ -3,4 +3,6 @@ ping_retries = 3 ping_timeout = 5 scan_threads = 300 config_threads = 300 -reboot_threads = 300 \ No newline at end of file +reboot_threads = 300 + +whatsminer_pwd = "admin" \ No newline at end of file