refactor: move ssh to miner.ssh

This commit is contained in:
upstreamdata
2024-01-18 15:32:09 -07:00
parent 3be3086a38
commit 4b5314a8f6
16 changed files with 155 additions and 110 deletions

View File

@@ -29,6 +29,7 @@ from pyasic.miners.base import (
WebAPICommand, WebAPICommand,
) )
from pyasic.rpc import APIError from pyasic.rpc import APIError
from pyasic.ssh.antminer import AntminerModernSSH
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = DataLocations( ANTMINER_MODERN_DATA_LOC = DataLocations(
@@ -85,6 +86,10 @@ class AntminerModern(BMMiner):
"""Handler for AntMiners with the modern web interface, such as S19""" """Handler for AntMiners with the modern web interface, such as S19"""
_web_cls = AntminerModernWebAPI _web_cls = AntminerModernWebAPI
web: AntminerModernWebAPI
_ssh_cls = AntminerModernSSH
ssh: AntminerModernSSH
data_locations = ANTMINER_MODERN_DATA_LOC data_locations = ANTMINER_MODERN_DATA_LOC
@@ -391,6 +396,7 @@ class AntminerOld(CGMiner):
"""Handler for AntMiners with the old web interface, such as S17""" """Handler for AntMiners with the old web interface, such as S17"""
_web_cls = AntminerOldWebAPI _web_cls = AntminerOldWebAPI
web: AntminerOldWebAPI
data_locations = ANTMINER_OLD_DATA_LOC data_locations = ANTMINER_OLD_DATA_LOC

View File

@@ -62,6 +62,7 @@ class BFGMiner(BaseMiner):
"""Base handler for BFGMiner based miners.""" """Base handler for BFGMiner based miners."""
_api_cls = BFGMinerRPCAPI _api_cls = BFGMinerRPCAPI
api: BFGMinerRPCAPI
data_locations = BFGMINER_DATA_LOC data_locations = BFGMINER_DATA_LOC

View File

@@ -67,6 +67,7 @@ class BMMiner(BaseMiner):
"""Base handler for BMMiner based miners.""" """Base handler for BMMiner based miners."""
_api_cls = BMMinerRPCAPI _api_cls = BMMinerRPCAPI
api: BMMinerRPCAPI
data_locations = BMMINER_DATA_LOC data_locations = BMMINER_DATA_LOC
@@ -80,14 +81,6 @@ class BMMiner(BaseMiner):
self.config = MinerConfig.from_api(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
async def reboot(self) -> bool:
logging.debug(f"{self}: Sending reboot command.")
ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
if ret is None:
return False
return True
################################################## ##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
@@ -122,9 +115,6 @@ class BMMiner(BaseMiner):
return self.fw_ver return self.fw_ver
async def _get_hostname(self) -> Optional[str]:
hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname")
return hn
async def _get_hashrate(self, api_summary: dict = None) -> Optional[float]: async def _get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API # get hr from API

View File

@@ -34,6 +34,7 @@ from pyasic.miners.base import (
WebAPICommand, WebAPICommand,
) )
from pyasic.rpc.bosminer import BOSMinerRPCAPI from pyasic.rpc.bosminer import BOSMinerRPCAPI
from pyasic.ssh.braiins_os import BOSMinerSSH
from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI
BOSMINER_DATA_LOC = DataLocations( BOSMINER_DATA_LOC = DataLocations(
@@ -98,7 +99,11 @@ class BOSMiner(BaseMiner):
"""Handler for old versions of BraiinsOS+ (pre-gRPC)""" """Handler for old versions of BraiinsOS+ (pre-gRPC)"""
_api_cls = BOSMinerRPCAPI _api_cls = BOSMinerRPCAPI
api: BOSMinerRPCAPI
_web_cls = BOSMinerWebAPI _web_cls = BOSMinerWebAPI
web: BOSMinerWebAPI
_ssh_cls = BOSMinerSSH
ssh: BOSMinerSSH
firmware = "BOS+" firmware = "BOS+"
@@ -108,7 +113,7 @@ class BOSMiner(BaseMiner):
supports_autotuning = True supports_autotuning = True
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
ret = await self.send_ssh_command("miner fault_light on") ret = await self.ssh.fault_light_on()
if isinstance(ret, str): if isinstance(ret, str):
self.light = True self.light = True
@@ -116,7 +121,7 @@ class BOSMiner(BaseMiner):
return False return False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
ret = await self.send_ssh_command("miner fault_light off") ret = await self.ssh.fault_light_off()
if isinstance(ret, str): if isinstance(ret, str):
self.light = False self.light = False
@@ -127,7 +132,7 @@ class BOSMiner(BaseMiner):
return await self.restart_bosminer() return await self.restart_bosminer()
async def restart_bosminer(self) -> bool: async def restart_bosminer(self) -> bool:
ret = await self.send_ssh_command("/etc/init.d/bosminer restart") ret = await self.ssh.restart_bosminer()
if isinstance(ret, str): if isinstance(ret, str):
return True return True
@@ -156,14 +161,14 @@ class BOSMiner(BaseMiner):
return False return False
async def reboot(self) -> bool: async def reboot(self) -> bool:
ret = await self.send_ssh_command("/sbin/reboot") ret = await self.ssh.reboot()
if isinstance(ret, str): if isinstance(ret, str):
return True return True
return False return False
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig:
raw_data = await self.send_ssh_command("cat /etc/bosminer.toml") raw_data = await self.ssh.get_config_file()
try: try:
toml_data = toml.loads(raw_data) toml_data = toml.loads(raw_data)
@@ -189,7 +194,7 @@ class BOSMiner(BaseMiner):
} }
) )
try: try:
conn = await self._get_ssh_connection() conn = await self.ssh._get_connection()
except ConnectionError as e: except ConnectionError as e:
raise APIError("SSH connection failed when sending config.") from e raise APIError("SSH connection failed when sending config.") from e
@@ -246,7 +251,7 @@ class BOSMiner(BaseMiner):
f"option dns '{dns}'", f"option dns '{dns}'",
] ]
) )
data = await self.send_ssh_command("cat /etc/config/network") data = await self.ssh.get_network_config()
split_data = data.split("\n\n") split_data = data.split("\n\n")
for idx, val in enumerate(split_data): for idx, val in enumerate(split_data):
@@ -254,7 +259,7 @@ class BOSMiner(BaseMiner):
split_data[idx] = cfg_data_lan split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data) config = "\n\n".join(split_data)
conn = await self._get_ssh_connection() conn = await self.ssh._get_connection()
async with conn: async with conn:
await conn.run("echo '" + config + "' > /etc/config/network") await conn.run("echo '" + config + "' > /etc/config/network")
@@ -268,7 +273,7 @@ class BOSMiner(BaseMiner):
"option proto 'dhcp'", "option proto 'dhcp'",
] ]
) )
data = await self.send_ssh_command("cat /etc/config/network") data = await self.ssh.get_network_config()
split_data = data.split("\n\n") split_data = data.split("\n\n")
for idx, val in enumerate(split_data): for idx, val in enumerate(split_data):
@@ -276,7 +281,7 @@ class BOSMiner(BaseMiner):
split_data[idx] = cfg_data_lan split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data) config = "\n\n".join(split_data)
conn = await self._get_ssh_connection() conn = await self.ssh._get_connection()
async with conn: async with conn:
await conn.run("echo '" + config + "' > /etc/config/network") await conn.run("echo '" + config + "' > /etc/config/network")
@@ -346,9 +351,7 @@ class BOSMiner(BaseMiner):
async def _get_hostname(self) -> Union[str, None]: async def _get_hostname(self) -> Union[str, None]:
try: try:
hostname = ( hostname = (await self.ssh.get_hostname()).strip()
await self.send_ssh_command("cat /proc/sys/kernel/hostname")
).strip()
except Exception as e: except Exception as e:
logging.error(f"{self} - Getting hostname failed: {e}") logging.error(f"{self} - Getting hostname failed: {e}")
return None return None
@@ -520,7 +523,7 @@ class BOSMiner(BaseMiner):
return self.light return self.light
try: try:
data = ( data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off") await self.ssh.get_led_status()
).strip() ).strip()
self.light = False self.light = False
if data == "50": if data == "50":
@@ -652,7 +655,9 @@ class BOSer(BaseMiner):
"""Handler for new versions of BraiinsOS+ (post-gRPC)""" """Handler for new versions of BraiinsOS+ (post-gRPC)"""
_api_cls = BOSMinerRPCAPI _api_cls = BOSMinerRPCAPI
web: BOSMinerRPCAPI
_web_cls = BOSerWebAPI _web_cls = BOSerWebAPI
web: BOSerWebAPI
data_locations = BOSER_DATA_LOC data_locations = BOSER_DATA_LOC

View File

@@ -116,7 +116,10 @@ BTMINER_DATA_LOC = DataLocations(
class BTMiner(BaseMiner): class BTMiner(BaseMiner):
"""Base handler for BTMiner based miners."""
_api_cls = BTMinerRPCAPI _api_cls = BTMinerRPCAPI
api: BTMinerRPCAPI
data_locations = BTMINER_DATA_LOC data_locations = BTMINER_DATA_LOC

View File

@@ -65,15 +65,10 @@ class CGMiner(BaseMiner):
"""Base handler for CGMiner based miners""" """Base handler for CGMiner based miners"""
_api_cls = CGMinerRPCAPI _api_cls = CGMinerRPCAPI
api: CGMinerRPCAPI
data_locations = CGMINER_DATA_LOC data_locations = CGMINER_DATA_LOC
async def reboot(self) -> bool:
ret = await self.send_ssh_command("reboot")
if ret is None:
return False
return True
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig:
# get pool data # get pool data
try: try:

View File

@@ -84,7 +84,10 @@ EPIC_DATA_LOC = DataLocations(
class ePIC(BaseMiner): class ePIC(BaseMiner):
"""Handler for miners with the ePIC board"""
_web_cls = ePICWebAPI _web_cls = ePICWebAPI
web: ePICWebAPI
firmware = "ePIC" firmware = "ePIC"

View File

@@ -67,7 +67,10 @@ GOLDSHELL_DATA_LOC = DataLocations(
class GoldshellMiner(BFGMiner): class GoldshellMiner(BFGMiner):
"""Handler for goldshell miners"""
_web_cls = GoldshellWebAPI _web_cls = GoldshellWebAPI
web: GoldshellWebAPI
data_locations = GOLDSHELL_DATA_LOC data_locations = GOLDSHELL_DATA_LOC

View File

@@ -98,6 +98,7 @@ class Innosilicon(CGMiner):
"""Base handler for Innosilicon miners""" """Base handler for Innosilicon miners"""
_web_cls = InnosiliconWebAPI _web_cls = InnosiliconWebAPI
web: InnosiliconWebAPI
data_locations = INNOSILICON_DATA_LOC data_locations = INNOSILICON_DATA_LOC

View File

@@ -61,7 +61,10 @@ LUXMINER_DATA_LOC = DataLocations(
class LUXMiner(BaseMiner): class LUXMiner(BaseMiner):
"""Handler for LuxOS miners"""
_api_cls = LUXMinerRPCAPI _api_cls = LUXMinerRPCAPI
api: LUXMinerRPCAPI
firmware = "LuxOS" firmware = "LuxOS"

View File

@@ -75,7 +75,10 @@ VNISH_DATA_LOC = DataLocations(
class VNish(BMMiner): class VNish(BMMiner):
"""Handler for VNish miners"""
_web_cls = VNishWebAPI _web_cls = VNishWebAPI
web: VNishWebAPI
firmware = "VNish" firmware = "VNish"

View File

@@ -15,13 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio import asyncio
import ipaddress import ipaddress
import logging
import warnings import warnings
from dataclasses import dataclass, field, make_dataclass from dataclasses import dataclass, field, make_dataclass
from enum import Enum from enum import Enum
from typing import Dict, List, Optional, Protocol, Tuple, Type, TypeVar, Union from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union
import asyncssh
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData from pyasic.data import Fan, HashBoard, MinerData
@@ -109,9 +106,12 @@ DataLocations = make_dataclass(
class MinerProtocol(Protocol): class MinerProtocol(Protocol):
_api_cls: Type = None _api_cls: Type = None
_web_cls: Type = None _web_cls: Type = None
_ssh_cls: Type = None
api = None ip: str = None
web = None api: _api_cls = None
web: _web_cls = None
ssh: _ssh_cls = None
make: str = None make: str = None
raw_model: str = None raw_model: str = None
@@ -150,69 +150,6 @@ class MinerProtocol(Protocol):
model_data.append(f"({self.firmware})") model_data.append(f"({self.firmware})")
return " ".join(model_data) return " ".join(model_data)
@property
def pwd(self) -> Dict[str, str]:
data = {}
if self.web is not None:
data["web"] = self.web.pwd
if self.api is not None:
data["api"] = self.api.pwd
return data
@pwd.setter
def pwd(self, val: str) -> None:
if self.web is not None:
self.web.pwd = val
if self.api is not None:
self.api.pwd = val
@property
def username(self) -> Dict[str, str]:
data = {}
if self.web is not None:
data["web"] = self.web.pwd
if self.api is not None:
data["api"] = self.api.pwd
return data
@username.setter
def username(self, val) -> None:
if self.web is not None:
self.web.username = val
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username="root",
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied as e:
raise ConnectionRefusedError from e
except Exception as e:
raise ConnectionError from e
async def send_ssh_command(self, cmd: str) -> Optional[str]:
"""Send an ssh command to the miner"""
try:
conn = await asyncio.wait_for(self._get_ssh_connection(), timeout=10)
except (ConnectionError, asyncio.TimeoutError):
return None
try:
async with conn:
resp = await conn.run(cmd)
result = str(max(resp.stdout, resp.stderr, key=len))
return result
except Exception as e:
logging.error(f"{self} command {cmd} error: {e}")
return None
async def check_light(self) -> bool: async def check_light(self) -> bool:
return await self.get_fault_light() return await self.get_fault_light()
@@ -597,9 +534,9 @@ class MinerProtocol(Protocol):
ip=str(self.ip), ip=str(self.ip),
make=self.make, make=self.make,
model=self.model, model=self.model,
expected_chips=self.expected_chips expected_chips=self.expected_chips * self.expected_hashboards
if self.expected_chips is not None if self.expected_chips is not None
else 0 * self.expected_hashboards, else 0,
expected_hashboards=self.expected_hashboards, expected_hashboards=self.expected_hashboards,
hashboards=[ hashboards=[
HashBoard(slot=i, expected_chips=self.expected_chips) HashBoard(slot=i, expected_chips=self.expected_chips)
@@ -632,6 +569,8 @@ class BaseMiner(MinerProtocol):
self.api = self._api_cls(ip) self.api = self._api_cls(ip)
if self._web_cls is not None: if self._web_cls is not None:
self.web = self._web_cls(ip) self.web = self._web_cls(ip)
if self._ssh_cls is not None:
self.ssh = self._ssh_cls(ip)
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -28,13 +28,15 @@ _settings = { # defaults
"factory_get_timeout": 3, "factory_get_timeout": 3,
"get_data_retries": 1, "get_data_retries": 1,
"api_function_timeout": 5, "api_function_timeout": 5,
"default_whatsminer_password": "admin", "default_whatsminer_rpc_password": "admin",
"default_innosilicon_password": "admin", "default_innosilicon_web_password": "admin",
"default_antminer_password": "root", "default_antminer_web_password": "root",
"default_bosminer_password": "root", "default_bosminer_web_password": "root",
"default_vnish_password": "admin", "default_vnish_web_password": "admin",
"default_goldshell_password": "123456789", "default_goldshell_web_password": "123456789",
"default_hive_password": "admin", "default_hive_web_password": "admin",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
"socket_linger_time": 1000, "socket_linger_time": 1000,
} }

47
pyasic/ssh/__init__.py Normal file
View File

@@ -0,0 +1,47 @@
import asyncio
import logging
from typing import Optional
import asyncssh
class MinerSSH:
def __init__(self, ip: str) -> None:
self.ip = ip
self.pwd = None
self.username = "root"
self.port = 22
async def _get_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
port=self.port,
known_hosts=None,
username=self.username,
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied as e:
raise ConnectionRefusedError from e
except Exception as e:
raise ConnectionError from e
async def send_command(self, cmd: str) -> Optional[str]:
"""Send an ssh command to the miner"""
try:
conn = await asyncio.wait_for(self._get_connection(), timeout=10)
except (ConnectionError, asyncio.TimeoutError):
return None
try:
async with conn:
resp = await conn.run(cmd)
result = str(max(resp.stdout, resp.stderr, key=len))
return result
except Exception as e:
logging.error(f"{self} command {cmd} error: {e}")
return None

9
pyasic/ssh/antminer.py Normal file
View File

@@ -0,0 +1,9 @@
from pyasic import settings
from pyasic.ssh import MinerSSH
class AntminerModernSSH(MinerSSH):
def __init__(self, ip: str):
super().__init__(ip)
self.pwd = settings.get("default_antminer_ssh_password", "root")
self.username = "miner"

35
pyasic/ssh/braiins_os.py Normal file
View File

@@ -0,0 +1,35 @@
from pyasic import settings
from pyasic.ssh import MinerSSH
class BOSMinerSSH(MinerSSH):
def __init__(self, ip: str):
super().__init__(ip)
self.pwd = settings.get("default_bosminer_ssh_password", "root")
async def get_board_info(self):
return await self.send_command("bosminer model -d")
async def fault_light_on(self):
return await self.send_command("miner fault_light on")
async def fault_light_off(self):
return await self.send_command("miner fault_light off")
async def restart_bosminer(self):
return await self.send_command("/etc/init.d/bosminer restart")
async def reboot(self):
return await self.send_command("/sbin/reboot")
async def get_config_file(self):
return await self.send_command("cat /etc/bosminer.toml")
async def get_network_config(self):
return await self.send_command("cat /etc/config/network")
async def get_hostname(self):
return await self.send_command("cat /proc/sys/kernel/hostname")
async def get_led_status(self):
return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off")