improve logging and some documentation
This commit is contained in:
@@ -42,7 +42,11 @@ class BaseMinerAPI:
|
|||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
|
||||||
def get_commands(self) -> list:
|
def get_commands(self) -> list:
|
||||||
"""Get a list of command accessible to a specific type of API on the miner."""
|
"""Get a list of command accessible to a specific type of API on the miner.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of all API commands that the miner supports.
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
func
|
func
|
||||||
for func in
|
for func in
|
||||||
@@ -60,42 +64,56 @@ class BaseMinerAPI:
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _check_commands(self, *commands):
|
||||||
|
allowed_commands = self.get_commands()
|
||||||
|
return_commands = []
|
||||||
|
for command in [*commands]:
|
||||||
|
if command in allowed_commands:
|
||||||
|
return_commands.append(command)
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
f"""Removing incorrect command: {command}
|
||||||
|
If you are sure you want to use this command please use API.send_command("{command}", ignore_errors=True) instead.""",
|
||||||
|
APIWarning,
|
||||||
|
)
|
||||||
|
return return_commands
|
||||||
|
|
||||||
async def multicommand(
|
async def multicommand(
|
||||||
self, *commands: str, ignore_x19_error: bool = False
|
self, *commands: str, ignore_x19_error: bool = False
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Creates and sends multiple commands as one command to the miner."""
|
"""Creates and sends multiple commands as one command to the miner.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
*commands: The commands to send as a multicommand to the miner.
|
||||||
|
ignore_x19_error: Whether or not to ignore errors raised by x19 miners when using the "+" delimited style.
|
||||||
|
"""
|
||||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
||||||
# split the commands into a proper list
|
# make sure we can actually run each command, otherwise they will fail
|
||||||
user_commands = [*commands]
|
commands = self._check_commands(*commands)
|
||||||
allowed_commands = self.get_commands()
|
|
||||||
# make sure we can actually run the command, otherwise it will fail
|
|
||||||
commands = [command for command in user_commands if command in allowed_commands]
|
|
||||||
for item in list(set(user_commands) - set(commands)):
|
|
||||||
warnings.warn(
|
|
||||||
f"""Removing incorrect command: {item}
|
|
||||||
If you are sure you want to use this command please use API.send_command("{item}", ignore_errors=True) instead.""",
|
|
||||||
APIWarning,
|
|
||||||
)
|
|
||||||
# standard multicommand format is "command1+command2"
|
# standard multicommand format is "command1+command2"
|
||||||
# doesnt work for S19 which is dealt with in the send command function
|
# doesnt work for S19 which uses the backup _x19_multicommand
|
||||||
command = "+".join(commands)
|
command = "+".join(commands)
|
||||||
data = None
|
|
||||||
try:
|
try:
|
||||||
data = await self.send_command(command, x19_command=ignore_x19_error)
|
data = await self.send_command(command, x19_command=ignore_x19_error)
|
||||||
except APIError:
|
except APIError:
|
||||||
try:
|
logging.debug(f"{self.ip}: Handling X19 multicommand.")
|
||||||
data = {}
|
data = await self._x19_multicommand(command.split("+"))
|
||||||
# S19 handler, try again
|
logging.debug(f"{self.ip}: Received multicommand data.")
|
||||||
for cmd in command.split("+"):
|
return data
|
||||||
data[cmd] = []
|
|
||||||
data[cmd].append(await self.send_command(cmd))
|
async def _x19_multicommand(self, *commands):
|
||||||
except APIError as e:
|
data = None
|
||||||
raise APIError(e)
|
try:
|
||||||
except Exception as e:
|
data = {}
|
||||||
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
|
# send all commands individually
|
||||||
if data:
|
for cmd in commands:
|
||||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
data[cmd] = []
|
||||||
return data
|
data[cmd].append(await self.send_command(cmd, x19_command=True))
|
||||||
|
except APIError as e:
|
||||||
|
raise APIError(e)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"{self.ip}: API Multicommand Error: {e.__name__} - {e}")
|
||||||
|
return data
|
||||||
|
|
||||||
async def send_command(
|
async def send_command(
|
||||||
self,
|
self,
|
||||||
@@ -104,7 +122,17 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
ignore_errors: bool = False,
|
ignore_errors: bool = False,
|
||||||
x19_command: bool = False,
|
x19_command: bool = False,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Send an API command to the miner and return the result."""
|
"""Send an API command to the miner and return the result.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command: The command to sent to the miner.
|
||||||
|
parameters: Any additional parameters to be sent with the command.
|
||||||
|
ignore_errors: Whether or not to raise APIError when the command returns an error.
|
||||||
|
x19_command: Whether this is a command for an x19 that may be an issue (such as a "+" delimited multicommand)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The return data from the API command parsed from JSON into a dict.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
# get reader and writer streams
|
||||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||||
@@ -116,7 +144,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
|
|
||||||
# create the command
|
# create the command
|
||||||
cmd = {"command": command}
|
cmd = {"command": command}
|
||||||
if parameters is not None:
|
if parameters:
|
||||||
cmd["parameter"] = parameters
|
cmd["parameter"] = parameters
|
||||||
|
|
||||||
# send the command
|
# send the command
|
||||||
@@ -134,9 +162,9 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
break
|
break
|
||||||
data += d
|
data += d
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"{self.ip}: API Command Error: {e}")
|
logging.warning(f"{self.ip}: API Command Error: {e.__name__} - {e}")
|
||||||
|
|
||||||
data = self.load_api_data(data)
|
data = self._load_api_data(data)
|
||||||
|
|
||||||
# close the connection
|
# close the connection
|
||||||
writer.close()
|
writer.close()
|
||||||
@@ -145,7 +173,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
# check for if the user wants to allow errors to return
|
# check for if the user wants to allow errors to return
|
||||||
if not ignore_errors:
|
if not ignore_errors:
|
||||||
# validate the command succeeded
|
# validate the command succeeded
|
||||||
validation = self.validate_command_output(data)
|
validation = self._validate_command_output(data)
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
if not x19_command:
|
if not x19_command:
|
||||||
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
|
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
|
||||||
@@ -154,8 +182,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_command_output(data: dict) -> tuple:
|
def _validate_command_output(data: dict) -> tuple:
|
||||||
"""Check if the returned command output is correctly formatted."""
|
|
||||||
# check if the data returned is correct or an error
|
# check if the data returned is correct or an error
|
||||||
# if status isn't a key, it is a multicommand
|
# if status isn't a key, it is a multicommand
|
||||||
if "STATUS" not in data.keys():
|
if "STATUS" not in data.keys():
|
||||||
@@ -182,8 +209,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_api_data(data: bytes) -> dict:
|
def _load_api_data(data: bytes) -> dict:
|
||||||
"""Convert API data from JSON to dict"""
|
|
||||||
str_data = None
|
str_data = None
|
||||||
try:
|
try:
|
||||||
# some json from the API returns with a null byte (\x00) on the end
|
# some json from the API returns with a null byte (\x00) on the end
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info(f"{str(self.ip)}: {e}")
|
logging.info(f"{str(self.ip)}: {e}")
|
||||||
|
|
||||||
data = self.load_api_data(data)
|
data = self._load_api_data(data)
|
||||||
|
|
||||||
# close the connection
|
# close the connection
|
||||||
writer.close()
|
writer.close()
|
||||||
@@ -225,7 +225,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
|
|
||||||
if not ignore_errors:
|
if not ignore_errors:
|
||||||
# if it fails to validate, it is likely an error
|
# if it fails to validate, it is likely an error
|
||||||
validation = self.validate_command_output(data)
|
validation = self._validate_command_output(data)
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
raise APIError(validation[1])
|
raise APIError(validation[1])
|
||||||
|
|
||||||
|
|||||||
@@ -54,13 +54,11 @@ class BaseMiner:
|
|||||||
)
|
)
|
||||||
return conn
|
return conn
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# logging.warning(f"{self} raised an exception: {e}")
|
|
||||||
raise e
|
raise e
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logging.warning(f"Connection refused: {self}")
|
logging.warning(f"Connection refused: {self}")
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# logging.warning(f"{self} raised an exception: {e}")
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def fault_light_on(self) -> bool:
|
async def fault_light_on(self) -> bool:
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
# create a list of tasks
|
# create a list of tasks
|
||||||
scan_tasks = []
|
scan_tasks = []
|
||||||
# for each miner IP that was passed in, add a task to get its class
|
# for each miner IP that was passed in, add a task to get its class
|
||||||
|
logging.debug(f"Getting miners with generator: [{ips}]")
|
||||||
for miner in ips:
|
for miner in ips:
|
||||||
scan_tasks.append(loop.create_task(self.get_miner(miner)))
|
scan_tasks.append(loop.create_task(self.get_miner(miner)))
|
||||||
# asynchronously run the tasks and return them as they complete
|
# asynchronously run the tasks and return them as they complete
|
||||||
@@ -273,6 +274,7 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
ip = ipaddress.ip_address(ip)
|
ip = ipaddress.ip_address(ip)
|
||||||
# check if the miner already exists in cache
|
# check if the miner already exists in cache
|
||||||
if ip in self.miners:
|
if ip in self.miners:
|
||||||
|
logging.debug(f"Miner exists in cache: {self.miners[ip]}")
|
||||||
return self.miners[ip]
|
return self.miners[ip]
|
||||||
# if everything fails, the miner is already set to unknown
|
# if everything fails, the miner is already set to unknown
|
||||||
miner = UnknownMiner(str(ip))
|
miner = UnknownMiner(str(ip))
|
||||||
@@ -296,6 +298,9 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
ver = new_ver
|
ver = new_ver
|
||||||
# if we find the API and model, don't need to loop anymore
|
# if we find the API and model, don't need to loop anymore
|
||||||
if api and model:
|
if api and model:
|
||||||
|
logging.debug(
|
||||||
|
f"Miner api and model found: API - {api}, Model - {model}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logging.warning(f"{ip}: Get Miner Timed Out")
|
logging.warning(f"{ip}: Get Miner Timed Out")
|
||||||
@@ -339,11 +344,13 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
self.miners[ip] = miner
|
self.miners[ip] = miner
|
||||||
|
|
||||||
# return the miner
|
# return the miner
|
||||||
|
logging.debug(f"Found miner: {miner}")
|
||||||
return miner
|
return miner
|
||||||
|
|
||||||
def clear_cached_miners(self) -> None:
|
def clear_cached_miners(self) -> None:
|
||||||
"""Clear the miner factory cache."""
|
"""Clear the miner factory cache."""
|
||||||
# empty out self.miners
|
# empty out self.miners
|
||||||
|
logging.debug("Clearing MinerFactory cache.")
|
||||||
self.miners = {}
|
self.miners = {}
|
||||||
|
|
||||||
async def _get_miner_type(
|
async def _get_miner_type(
|
||||||
@@ -369,12 +376,16 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
version = data["version"][0]
|
version = data["version"][0]
|
||||||
|
|
||||||
except APIError:
|
except APIError:
|
||||||
|
logging.debug(f"API Error when getting miner type: {str(ip)}")
|
||||||
try:
|
try:
|
||||||
# try devdetails and version separately (X19s mainly require this)
|
# try devdetails and version separately (X19s mainly require this)
|
||||||
# get devdetails and validate
|
# get devdetails and validate
|
||||||
devdetails = await self._send_api_command(str(ip), "devdetails")
|
devdetails = await self._send_api_command(str(ip), "devdetails")
|
||||||
validation = await self._validate_command(devdetails)
|
validation = await self._validate_command(devdetails)
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
|
logging.debug(
|
||||||
|
f"Splitting commands failed when getting miner type: {str(ip)}"
|
||||||
|
)
|
||||||
# if devdetails fails try version instead
|
# if devdetails fails try version instead
|
||||||
devdetails = None
|
devdetails = None
|
||||||
|
|
||||||
@@ -388,12 +399,15 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
|
|
||||||
# if this fails we raise an error to be caught below
|
# if this fails we raise an error to be caught below
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
|
logging.debug(
|
||||||
|
f"get_version failed when getting miner type: {str(ip)}"
|
||||||
|
)
|
||||||
raise APIError(validation[1])
|
raise APIError(validation[1])
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
# catch APIError and let the factory know we cant get data
|
# catch APIError and let the factory know we cant get data
|
||||||
logging.warning(f"{ip}: API Command Error: {e}")
|
logging.warning(f"{ip}: API Command Error: {e.__name__} - {e}")
|
||||||
return None, None, None
|
return None, None, None
|
||||||
except OSError as e:
|
except OSError:
|
||||||
# miner refused connection on API port, we wont be able to get data this way
|
# miner refused connection on API port, we wont be able to get data this way
|
||||||
# try ssh
|
# try ssh
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user