improve logging and some documentation

This commit is contained in:
UpstreamData
2022-07-18 14:38:54 -06:00
parent 98e2cfae84
commit 43b4992cee
4 changed files with 81 additions and 43 deletions

View File

@@ -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,41 +64,55 @@ 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:
logging.debug(f"{self.ip}: Handling X19 multicommand.")
data = await self._x19_multicommand(command.split("+"))
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _x19_multicommand(self, *commands):
data = None
try: try:
data = {} data = {}
# S19 handler, try again # send all commands individually
for cmd in command.split("+"): for cmd in commands:
data[cmd] = [] data[cmd] = []
data[cmd].append(await self.send_command(cmd)) data[cmd].append(await self.send_command(cmd, x19_command=True))
except APIError as e: except APIError as e:
raise APIError(e) raise APIError(e)
except Exception as e: except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e}") logging.warning(f"{self.ip}: API Multicommand Error: {e.__name__} - {e}")
if data:
logging.debug(f"{self.ip}: Received multicommand data.")
return data return data
async def send_command( async def send_command(
@@ -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

View File

@@ -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])

View File

@@ -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:

View File

@@ -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: