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

View File

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

View File

@@ -67,6 +67,7 @@ class BMMiner(BaseMiner):
"""Base handler for BMMiner based miners."""
_api_cls = BMMinerRPCAPI
api: BMMinerRPCAPI
data_locations = BMMINER_DATA_LOC
@@ -80,14 +81,6 @@ class BMMiner(BaseMiner):
self.config = MinerConfig.from_api(pools)
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}) ###
##################################################
@@ -122,9 +115,6 @@ class BMMiner(BaseMiner):
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]:
# get hr from API

View File

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

View File

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

View File

@@ -65,15 +65,10 @@ class CGMiner(BaseMiner):
"""Base handler for CGMiner based miners"""
_api_cls = CGMinerRPCAPI
api: CGMinerRPCAPI
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:
# get pool data
try:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,13 +15,10 @@
# ------------------------------------------------------------------------------
import asyncio
import ipaddress
import logging
import warnings
from dataclasses import dataclass, field, make_dataclass
from enum import Enum
from typing import Dict, List, Optional, Protocol, Tuple, Type, TypeVar, Union
import asyncssh
from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
@@ -109,9 +106,12 @@ DataLocations = make_dataclass(
class MinerProtocol(Protocol):
_api_cls: Type = None
_web_cls: Type = None
_ssh_cls: Type = None
api = None
web = None
ip: str = None
api: _api_cls = None
web: _web_cls = None
ssh: _ssh_cls = None
make: str = None
raw_model: str = None
@@ -150,69 +150,6 @@ class MinerProtocol(Protocol):
model_data.append(f"({self.firmware})")
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:
return await self.get_fault_light()
@@ -597,9 +534,9 @@ class MinerProtocol(Protocol):
ip=str(self.ip),
make=self.make,
model=self.model,
expected_chips=self.expected_chips
expected_chips=self.expected_chips * self.expected_hashboards
if self.expected_chips is not None
else 0 * self.expected_hashboards,
else 0,
expected_hashboards=self.expected_hashboards,
hashboards=[
HashBoard(slot=i, expected_chips=self.expected_chips)
@@ -632,6 +569,8 @@ class BaseMiner(MinerProtocol):
self.api = self._api_cls(ip)
if self._web_cls is not None:
self.web = self._web_cls(ip)
if self._ssh_cls is not None:
self.ssh = self._ssh_cls(ip)
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -28,13 +28,15 @@ _settings = { # defaults
"factory_get_timeout": 3,
"get_data_retries": 1,
"api_function_timeout": 5,
"default_whatsminer_password": "admin",
"default_innosilicon_password": "admin",
"default_antminer_password": "root",
"default_bosminer_password": "root",
"default_vnish_password": "admin",
"default_goldshell_password": "123456789",
"default_hive_password": "admin",
"default_whatsminer_rpc_password": "admin",
"default_innosilicon_web_password": "admin",
"default_antminer_web_password": "root",
"default_bosminer_web_password": "root",
"default_vnish_web_password": "admin",
"default_goldshell_web_password": "123456789",
"default_hive_web_password": "admin",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
"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")