From 1ab05c7a5e9e223159d0a241b1d7e0c47275d0db Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Mon, 20 May 2024 23:43:39 +0530 Subject: [PATCH 01/10] Add firmare update funtionality --- pyasic/miners/base.py | 116 +++++++++++++++++++++++++++++++ tests/miners_tests/tests_base.py | 1 + 2 files changed, 117 insertions(+) create mode 100644 tests/miners_tests/tests_base.py diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index f909d378..6dea493c 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -16,6 +16,9 @@ import asyncio import ipaddress import warnings +import aiohttp +import tempfile +import os from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union from pyasic.config import MinerConfig @@ -519,6 +522,51 @@ class MinerProtocol(Protocol): return data + async def update_firmware(self, firmware_url: str) -> bool: + """ + Update the firmware of the miner. + + Parameters: + firmware_url: The URL of the firmware to download and apply to the miner. + + Returns: + A boolean value indicating the success of the update process. + """ + # Verify if the miner type is supported + # TODO + + # Determine if a server URL is provided and query for firmware, otherwise use the direct URL + if firmware_url.startswith("http"): + latest_firmware_info = await self._fetch_firmware_info(firmware_url) + else: + latest_firmware_info = await self._query_firmware_server(firmware_url) + if not latest_firmware_info: + raise ValueError("Could not fetch firmware information.") + + # Check the current firmware version on the miner + current_firmware_version = await self.get_fw_ver() + if current_firmware_version == latest_firmware_info['version']: + return True # Firmware is already up to date + + # Download the new firmware file + firmware_file_path = await self._download_firmware(latest_firmware_info['download_url']) + if not firmware_file_path: + raise IOError("Failed to download the firmware file.") + + # Transfer the firmware file to the miner + # TODO + + # Apply the firmware update on the miner + # TODO + + # Reboot the miner + # TODO + + # Verify the update success by polling the firmware version + # TODO + + return True + class BaseMiner(MinerProtocol): def __init__(self, ip: str) -> None: @@ -538,5 +586,73 @@ class BaseMiner(MinerProtocol): if self._ssh_cls is not None: self.ssh = self._ssh_cls(ip) + async def _fetch_firmware_info(self, firmware_url: str) -> dict: + """ + Fetch the latest firmware information from the given URL. + + Parameters: + firmware_url: The URL to fetch the firmware information from. + + Returns: + A dictionary containing the firmware version and download URL. + """ + async with aiohttp.ClientSession() as session: + async with session.get(firmware_url) as response: + if response.status != 200: + raise ConnectionError(f"Failed to fetch firmware info, status code: {response.status}") + firmware_info = await response.json() + return { + 'version': firmware_info['version'], + 'download_url': firmware_info['download_url'] + } + + async def _download_firmware(self, download_url: str) -> str: + """ + Download the firmware file from the given URL and save it to a temporary location. + + Parameters: + download_url: The URL to download the firmware from. + + Returns: + The file path to the downloaded firmware file. + """ + async with aiohttp.ClientSession() as session: + async with session.get(download_url) as response: + if response.status != 200: + raise ConnectionError(f"Failed to download firmware, status code: {response.status}") + # Assuming the Content-Disposition header contains the filename + content_disposition = response.headers.get('Content-Disposition') + if not content_disposition: + raise ValueError("Could not determine the filename of the firmware download.") + filename = content_disposition.split("filename=")[-1].strip().strip('"') + temp_dir = tempfile.gettempdir() + file_path = os.path.join(temp_dir, filename) + with open(file_path, 'wb') as file: + file.write(await response.read()) + return file_path + + async def _query_firmware_server(self, server_url: str) -> dict: + """ + Query the firmware server for available firmware files. + + Parameters: + server_url: The URL of the server to query for firmware files. + + Returns: + A dictionary containing the firmware version and download URL. + """ + async with aiohttp.ClientSession() as session: + async with session.get(server_url) as response: + if response.status != 200: + raise ConnectionError(f"Failed to query firmware server, status code: {response.status}") + firmware_files = await response.json() + for firmware_file in firmware_files: + if self._is_appropriate_firmware(firmware_file['version']): + return { + 'version': firmware_file['version'], + 'download_url': firmware_file['url'] + } + raise ValueError("No appropriate firmware file found on the server.") + AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) diff --git a/tests/miners_tests/tests_base.py b/tests/miners_tests/tests_base.py new file mode 100644 index 00000000..8b78dda5 --- /dev/null +++ b/tests/miners_tests/tests_base.py @@ -0,0 +1 @@ +# Tests for update_firmware \ No newline at end of file From 2d92c2c0e27ee0dad5ebfdd9f5803745ea2a6dd8 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Sun, 26 May 2024 02:22:21 +0530 Subject: [PATCH 02/10] Replace aiohttp with httpx and add the next steps for `update_firmware` --- pyasic/miners/base.py | 183 ++++++++++++++++++++++++++++++++---------- 1 file changed, 141 insertions(+), 42 deletions(-) diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index 6dea493c..b61233b6 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -16,7 +16,7 @@ import asyncio import ipaddress import warnings -import aiohttp +import httpx import tempfile import os from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union @@ -548,16 +548,23 @@ class MinerProtocol(Protocol): if current_firmware_version == latest_firmware_info['version']: return True # Firmware is already up to date - # Download the new firmware file - firmware_file_path = await self._download_firmware(latest_firmware_info['download_url']) - if not firmware_file_path: - raise IOError("Failed to download the firmware file.") + # Download the new firmware file if it's not a local file + if not firmware_url.startswith("file://"): + firmware_file_path = await self._download_firmware(latest_firmware_info['download_url']) + if not firmware_file_path: + raise IOError("Failed to download the firmware file.") + else: + firmware_file_path = latest_firmware_info['download_url'] # Transfer the firmware file to the miner - # TODO + transfer_success = await self._transfer_firmware_to_miner(firmware_file_path) + if not transfer_success: + raise IOError("Failed to transfer the firmware file to the miner.") # Apply the firmware update on the miner - # TODO + update_success = await self._apply_firmware_update(firmware_file_path) + if not update_success: + raise IOError("Failed to apply the firmware update.") # Reboot the miner # TODO @@ -596,15 +603,15 @@ class BaseMiner(MinerProtocol): Returns: A dictionary containing the firmware version and download URL. """ - async with aiohttp.ClientSession() as session: - async with session.get(firmware_url) as response: - if response.status != 200: - raise ConnectionError(f"Failed to fetch firmware info, status code: {response.status}") - firmware_info = await response.json() - return { - 'version': firmware_info['version'], - 'download_url': firmware_info['download_url'] - } + async with httpx.AsyncClient() as client: + response = await client.get(firmware_url) + if response.status_code != 200: + raise ConnectionError(f"Failed to fetch firmware info, status code: {response.status_code}") + firmware_info = response.json() + return { + 'version': firmware_info['version'], + 'download_url': firmware_info['download_url'] + } async def _download_firmware(self, download_url: str) -> str: """ @@ -616,20 +623,24 @@ class BaseMiner(MinerProtocol): Returns: The file path to the downloaded firmware file. """ - async with aiohttp.ClientSession() as session: - async with session.get(download_url) as response: - if response.status != 200: - raise ConnectionError(f"Failed to download firmware, status code: {response.status}") - # Assuming the Content-Disposition header contains the filename - content_disposition = response.headers.get('Content-Disposition') - if not content_disposition: - raise ValueError("Could not determine the filename of the firmware download.") - filename = content_disposition.split("filename=")[-1].strip().strip('"') - temp_dir = tempfile.gettempdir() - file_path = os.path.join(temp_dir, filename) - with open(file_path, 'wb') as file: - file.write(await response.read()) - return file_path + async with httpx.AsyncClient() as client: + response = await client.get(download_url) + if response.status_code != 200: + raise ConnectionError(f"Failed to download firmware, status code: {response.status_code}") + + # Assuming the Content-Disposition header contains the filename + content_disposition = response.headers.get('Content-Disposition') + if not content_disposition: + raise ValueError("Could not determine the filename of the firmware download.") + + filename = content_disposition.split("filename=")[-1].strip().strip('"') + temp_dir = tempfile.gettempdir() + file_path = os.path.join(temp_dir, filename) + + with open(file_path, 'wb') as file: + file.write(response.content) + + return file_path async def _query_firmware_server(self, server_url: str) -> dict: """ @@ -641,18 +652,106 @@ class BaseMiner(MinerProtocol): Returns: A dictionary containing the firmware version and download URL. """ - async with aiohttp.ClientSession() as session: - async with session.get(server_url) as response: - if response.status != 200: - raise ConnectionError(f"Failed to query firmware server, status code: {response.status}") - firmware_files = await response.json() - for firmware_file in firmware_files: - if self._is_appropriate_firmware(firmware_file['version']): - return { - 'version': firmware_file['version'], - 'download_url': firmware_file['url'] - } - raise ValueError("No appropriate firmware file found on the server.") + async with httpx.AsyncClient() as client: + response = await client.get(server_url) + if response.status_code != 200: + raise ConnectionError(f"Failed to query firmware server, status code: {response.status_code}") + firmware_files = response.json() + for firmware_file in firmware_files: + if self._is_appropriate_firmware(firmware_file['version']): + return { + 'version': firmware_file['version'], + 'download_url': firmware_file['url'] + } + raise ValueError("No appropriate firmware file found on the server.") + async def _transfer_firmware_to_miner(self, firmware_file_path: str) -> bool: + """ + Transfer the firmware file to the miner using SSH. + + Parameters: + firmware_file_path: The file path to the firmware file that needs to be transferred. + + Returns: + A boolean value indicating the success of the transfer. + """ + import paramiko + from paramiko import SSHClient + from scp import SCPClient + + # Retrieve SSH credentials from environment variables or secure storage + ssh_username = os.getenv('MINER_SSH_USERNAME') + ssh_password = os.getenv('MINER_SSH_PASSWORD') + ssh_key_path = os.getenv('MINER_SSH_KEY_PATH') + + try: + # Establish an SSH client session using credentials + with SSHClient() as ssh_client: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + if ssh_key_path: + ssh_client.connect(self.ip, username=ssh_username, key_filename=ssh_key_path) + else: + ssh_client.connect(self.ip, username=ssh_username, password=ssh_password) + + # Use SCPClient to transfer the firmware file to the miner + with SCPClient(ssh_client.get_transport()) as scp_client: + scp_client.put(firmware_file_path, remote_path='/tmp/') + + return True + except Exception as e: + logger.error(f"Failed to transfer firmware to miner {self.ip}: {e}") + return False + + async def _apply_firmware_update(self, firmware_file_path: str) -> bool: + """ + Apply the firmware update to the miner. + + Parameterrs: + firmware_file_path: The path to the firmware file to be uploaded. + + Returns: + True if the firmware update was successful, False otherwise. + """ + try: + # Identify the miner using the get_miner function + miner = await self.get_miner(self.ip) + if not miner: + logger.error(f"Failed to identify miner at {self.ip}") + return False + + # Gather necessary data from the miner + miner_data = await miner.get_data() + + # Logic to apply firmware update using SSH + if miner.ssh: + async with miner.ssh as ssh_client: + await ssh_client.upload_file(firmware_file_path, "/tmp/firmware.bin") + await ssh_client.execute_command("upgrade_firmware /tmp/firmware.bin") + return True + # Logic to apply firmware update using RPC + elif miner.rpc: + await miner.rpc.upload_firmware(firmware_file_path) + await miner.rpc.execute_command("upgrade_firmware") + return True + else: + logger.error("No valid interface available to apply firmware update.") + return False + except Exception as e: + logger.error(f"Error applying firmware update: {e}") + return False + + async def _is_appropriate_firmware(self, firmware_version: str) -> bool: + """ + Determine if the given firmware version is appropriate for the miner. + + Parameters: + firmware_version: The version of the firmware to evaluate. + + Returns: + A boolean indicating if the firmware version is appropriate. + """ + # TODO + return True AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) From dbdd23e37d07da7f28e52d3286c772615bc2c9e2 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Tue, 28 May 2024 16:12:02 +0530 Subject: [PATCH 03/10] Moved update_firmware from base.py to braiins_os.py --- pyasic/miners/base.py | 217 +------------------------------ pyasic/ssh/braiins_os.py | 12 ++ tests/miners_tests/tests_base.py | 1 - 3 files changed, 13 insertions(+), 217 deletions(-) delete mode 100644 tests/miners_tests/tests_base.py diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index b61233b6..4a8b5840 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -16,9 +16,6 @@ import asyncio import ipaddress import warnings -import httpx -import tempfile -import os from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union from pyasic.config import MinerConfig @@ -522,58 +519,6 @@ class MinerProtocol(Protocol): return data - async def update_firmware(self, firmware_url: str) -> bool: - """ - Update the firmware of the miner. - - Parameters: - firmware_url: The URL of the firmware to download and apply to the miner. - - Returns: - A boolean value indicating the success of the update process. - """ - # Verify if the miner type is supported - # TODO - - # Determine if a server URL is provided and query for firmware, otherwise use the direct URL - if firmware_url.startswith("http"): - latest_firmware_info = await self._fetch_firmware_info(firmware_url) - else: - latest_firmware_info = await self._query_firmware_server(firmware_url) - if not latest_firmware_info: - raise ValueError("Could not fetch firmware information.") - - # Check the current firmware version on the miner - current_firmware_version = await self.get_fw_ver() - if current_firmware_version == latest_firmware_info['version']: - return True # Firmware is already up to date - - # Download the new firmware file if it's not a local file - if not firmware_url.startswith("file://"): - firmware_file_path = await self._download_firmware(latest_firmware_info['download_url']) - if not firmware_file_path: - raise IOError("Failed to download the firmware file.") - else: - firmware_file_path = latest_firmware_info['download_url'] - - # Transfer the firmware file to the miner - transfer_success = await self._transfer_firmware_to_miner(firmware_file_path) - if not transfer_success: - raise IOError("Failed to transfer the firmware file to the miner.") - - # Apply the firmware update on the miner - update_success = await self._apply_firmware_update(firmware_file_path) - if not update_success: - raise IOError("Failed to apply the firmware update.") - - # Reboot the miner - # TODO - - # Verify the update success by polling the firmware version - # TODO - - return True - class BaseMiner(MinerProtocol): def __init__(self, ip: str) -> None: @@ -593,165 +538,5 @@ class BaseMiner(MinerProtocol): if self._ssh_cls is not None: self.ssh = self._ssh_cls(ip) - async def _fetch_firmware_info(self, firmware_url: str) -> dict: - """ - Fetch the latest firmware information from the given URL. - Parameters: - firmware_url: The URL to fetch the firmware information from. - - Returns: - A dictionary containing the firmware version and download URL. - """ - async with httpx.AsyncClient() as client: - response = await client.get(firmware_url) - if response.status_code != 200: - raise ConnectionError(f"Failed to fetch firmware info, status code: {response.status_code}") - firmware_info = response.json() - return { - 'version': firmware_info['version'], - 'download_url': firmware_info['download_url'] - } - - async def _download_firmware(self, download_url: str) -> str: - """ - Download the firmware file from the given URL and save it to a temporary location. - - Parameters: - download_url: The URL to download the firmware from. - - Returns: - The file path to the downloaded firmware file. - """ - async with httpx.AsyncClient() as client: - response = await client.get(download_url) - if response.status_code != 200: - raise ConnectionError(f"Failed to download firmware, status code: {response.status_code}") - - # Assuming the Content-Disposition header contains the filename - content_disposition = response.headers.get('Content-Disposition') - if not content_disposition: - raise ValueError("Could not determine the filename of the firmware download.") - - filename = content_disposition.split("filename=")[-1].strip().strip('"') - temp_dir = tempfile.gettempdir() - file_path = os.path.join(temp_dir, filename) - - with open(file_path, 'wb') as file: - file.write(response.content) - - return file_path - - async def _query_firmware_server(self, server_url: str) -> dict: - """ - Query the firmware server for available firmware files. - - Parameters: - server_url: The URL of the server to query for firmware files. - - Returns: - A dictionary containing the firmware version and download URL. - """ - async with httpx.AsyncClient() as client: - response = await client.get(server_url) - if response.status_code != 200: - raise ConnectionError(f"Failed to query firmware server, status code: {response.status_code}") - firmware_files = response.json() - for firmware_file in firmware_files: - if self._is_appropriate_firmware(firmware_file['version']): - return { - 'version': firmware_file['version'], - 'download_url': firmware_file['url'] - } - raise ValueError("No appropriate firmware file found on the server.") - - async def _transfer_firmware_to_miner(self, firmware_file_path: str) -> bool: - """ - Transfer the firmware file to the miner using SSH. - - Parameters: - firmware_file_path: The file path to the firmware file that needs to be transferred. - - Returns: - A boolean value indicating the success of the transfer. - """ - import paramiko - from paramiko import SSHClient - from scp import SCPClient - - # Retrieve SSH credentials from environment variables or secure storage - ssh_username = os.getenv('MINER_SSH_USERNAME') - ssh_password = os.getenv('MINER_SSH_PASSWORD') - ssh_key_path = os.getenv('MINER_SSH_KEY_PATH') - - try: - # Establish an SSH client session using credentials - with SSHClient() as ssh_client: - ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - if ssh_key_path: - ssh_client.connect(self.ip, username=ssh_username, key_filename=ssh_key_path) - else: - ssh_client.connect(self.ip, username=ssh_username, password=ssh_password) - - # Use SCPClient to transfer the firmware file to the miner - with SCPClient(ssh_client.get_transport()) as scp_client: - scp_client.put(firmware_file_path, remote_path='/tmp/') - - return True - except Exception as e: - logger.error(f"Failed to transfer firmware to miner {self.ip}: {e}") - return False - - async def _apply_firmware_update(self, firmware_file_path: str) -> bool: - """ - Apply the firmware update to the miner. - - Parameterrs: - firmware_file_path: The path to the firmware file to be uploaded. - - Returns: - True if the firmware update was successful, False otherwise. - """ - try: - # Identify the miner using the get_miner function - miner = await self.get_miner(self.ip) - if not miner: - logger.error(f"Failed to identify miner at {self.ip}") - return False - - # Gather necessary data from the miner - miner_data = await miner.get_data() - - # Logic to apply firmware update using SSH - if miner.ssh: - async with miner.ssh as ssh_client: - await ssh_client.upload_file(firmware_file_path, "/tmp/firmware.bin") - await ssh_client.execute_command("upgrade_firmware /tmp/firmware.bin") - return True - # Logic to apply firmware update using RPC - elif miner.rpc: - await miner.rpc.upload_firmware(firmware_file_path) - await miner.rpc.execute_command("upgrade_firmware") - return True - else: - logger.error("No valid interface available to apply firmware update.") - return False - except Exception as e: - logger.error(f"Error applying firmware update: {e}") - return False - - async def _is_appropriate_firmware(self, firmware_version: str) -> bool: - """ - Determine if the given firmware version is appropriate for the miner. - - Parameters: - firmware_version: The version of the firmware to evaluate. - - Returns: - A boolean indicating if the firmware version is appropriate. - """ - # TODO - return True - -AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) +AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) \ No newline at end of file diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index 6dee3502..11770f60 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -93,3 +93,15 @@ class BOSMinerSSH(BaseSSH): str: Status of the LED. """ return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off") + + async def upgrade_firmware(self, file: str): + """Upgrade the firmware of the BrainOS miner using a specified file. + + This function upgrades the firmware of the BrainOS miner using a specified file. + It downloads the firmware file to a temporary location and performs the upgrade. + + Args: + file (str): The URL or local path of the firmware file. + + """ + pass \ No newline at end of file diff --git a/tests/miners_tests/tests_base.py b/tests/miners_tests/tests_base.py deleted file mode 100644 index 8b78dda5..00000000 --- a/tests/miners_tests/tests_base.py +++ /dev/null @@ -1 +0,0 @@ -# Tests for update_firmware \ No newline at end of file From 6458a71b5d18d82d3cda70c430a6a77bf314459a Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Wed, 29 May 2024 01:20:37 +0530 Subject: [PATCH 04/10] Add upgrade_firmware for BOS miner. --- pyasic/ssh/braiins_os.py | 80 ++++++++++++++++++++++++--- tests/miners_tests/test_braiins_os.py | 24 ++++++++ 2 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 tests/miners_tests/test_braiins_os.py diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index 11770f60..9b42fa63 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -1,6 +1,16 @@ from pyasic import settings from pyasic.ssh.base import BaseSSH +import logging +import requests +import os +# Set up logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) class BOSMinerSSH(BaseSSH): def __init__(self, ip: str): @@ -94,14 +104,70 @@ class BOSMinerSSH(BaseSSH): """ return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off") - async def upgrade_firmware(self, file: str): - """Upgrade the firmware of the BrainOS miner using a specified file. - - This function upgrades the firmware of the BrainOS miner using a specified file. - It downloads the firmware file to a temporary location and performs the upgrade. + async def upgrade_firmware(self, file_location: str = None): + """ + Upgrade the firmware of the BOSMiner device. Args: - file (str): The URL or local path of the firmware file. + file_location (str): The local file path of the firmware to be uploaded. If not provided, the firmware will be downloaded from the internal server. + Returns: + str: Confirmation message after upgrading the firmware. """ - pass \ No newline at end of file + try: + logger.info("Starting firmware upgrade process.") + + if file_location is None: + # Check for cached firmware file + cached_file_location = "/tmp/cached_firmware.tar.gz" + if os.path.exists(cached_file_location): + logger.info("Cached firmware file found. Checking version.") + # Compare cached firmware version with the latest version on the server + response = requests.get("http://firmware.pyasic.org/latest") + response.raise_for_status() + latest_version = response.json().get("version") + cached_version = self._get_fw_ver() + + if cached_version == latest_version: + logger.info("Cached firmware version matches the latest version. Using cached file.") + file_location = cached_file_location + else: + logger.info("Cached firmware version does not match the latest version. Downloading new version.") + firmware_url = response.json().get("url") + if not firmware_url: + raise ValueError("Firmware URL not found in the server response.") + firmware_response = requests.get(firmware_url) + firmware_response.raise_for_status() + with open(cached_file_location, "wb") as firmware_file: + firmware_file.write(firmware_response.content) + file_location = cached_file_location + else: + logger.info("No cached firmware file found. Downloading new version.") + response = requests.get("http://firmware.pyasic.org/latest") + response.raise_for_status() + firmware_url = response.json().get("url") + if not firmware_url: + raise ValueError("Firmware URL not found in the server response.") + firmware_response = requests.get(firmware_url) + firmware_response.raise_for_status() + with open(cached_file_location, "wb") as firmware_file: + firmware_file.write(firmware_response.content) + file_location = cached_file_location + + # Upload the firmware file to the BOSMiner device + logger.info(f"Uploading firmware file from {file_location} to the device.") + await self.send_command(f"scp {file_location} root@{self.ip}:/tmp/firmware.tar.gz") + + # Extract the firmware file + logger.info("Extracting the firmware file on the device.") + await self.send_command("tar -xzf /tmp/firmware.tar.gz -C /tmp") + + # Run the firmware upgrade script + logger.info("Running the firmware upgrade script on the device.") + result = await self.send_command("sh /tmp/upgrade_firmware.sh") + + logger.info("Firmware upgrade process completed successfully.") + return result + except Exception as e: + logger.error(f"An error occurred during the firmware upgrade process: {e}") + raise \ No newline at end of file diff --git a/tests/miners_tests/test_braiins_os.py b/tests/miners_tests/test_braiins_os.py new file mode 100644 index 00000000..b3a4eb1e --- /dev/null +++ b/tests/miners_tests/test_braiins_os.py @@ -0,0 +1,24 @@ +import pytest +from unittest.mock import patch, mock_open +from pyasic.ssh.braiins_os import BOSMinerSSH + +@pytest.fixture +def bosminer_ssh(): + return BOSMinerSSH(ip="192.168.1.100") + +@pytest.mark.asyncio +async def test_upgrade_firmware_with_valid_file_location(bosminer_ssh): + with patch("pyasic.ssh.braiins_os.os.path.exists") as mock_exists, \ + patch("pyasic.ssh.braiins_os.open", mock_open(read_data="data")) as mock_file, \ + patch("pyasic.ssh.braiins_os.requests.get") as mock_get, \ + patch.object(bosminer_ssh, "send_command") as mock_send_command: + + mock_exists.return_value = False + file_location = "/path/to/firmware.tar.gz" + + result = await bosminer_ssh.upgrade_firmware(file_location=file_location) + + mock_send_command.assert_any_call(f"scp {file_location} root@{bosminer_ssh.ip}:/tmp/firmware.tar.gz") + mock_send_command.assert_any_call("tar -xzf /tmp/firmware.tar.gz -C /tmp") + mock_send_command.assert_any_call("sh /tmp/upgrade_firmware.sh") + assert result is not None \ No newline at end of file From 8f0cf5b3a3ed6a7006556edc3b38891cd282ec19 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Wed, 29 May 2024 02:05:58 +0530 Subject: [PATCH 05/10] Made some changes in the code based on reviews --- pyasic/ssh/braiins_os.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index 9b42fa63..c796adff 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -1,7 +1,9 @@ +import tempfile from pyasic import settings from pyasic.ssh.base import BaseSSH import logging -import requests +import httpx +from pathlib import Path import os # Set up logging @@ -119,40 +121,44 @@ class BOSMinerSSH(BaseSSH): if file_location is None: # Check for cached firmware file - cached_file_location = "/tmp/cached_firmware.tar.gz" - if os.path.exists(cached_file_location): + with tempfile.NamedTemporaryFile(delete=False) as temp_firmware_file: + cached_file_location = Path(temp_firmware_file.name) + if cached_file_location.exists(): logger.info("Cached firmware file found. Checking version.") # Compare cached firmware version with the latest version on the server - response = requests.get("http://firmware.pyasic.org/latest") + async with httpx.AsyncClient() as client: + response = await client.get("http://firmware.pyasic.org/latest") response.raise_for_status() latest_version = response.json().get("version") cached_version = self._get_fw_ver() - if cached_version == latest_version: logger.info("Cached firmware version matches the latest version. Using cached file.") - file_location = cached_file_location + file_location = str(cached_file_location) else: logger.info("Cached firmware version does not match the latest version. Downloading new version.") firmware_url = response.json().get("url") if not firmware_url: raise ValueError("Firmware URL not found in the server response.") - firmware_response = requests.get(firmware_url) + async with httpx.AsyncClient() as client: + firmware_response = await client.get(firmware_url) firmware_response.raise_for_status() - with open(cached_file_location, "wb") as firmware_file: + with cached_file_location.open("wb") as firmware_file: firmware_file.write(firmware_response.content) - file_location = cached_file_location + file_location = str(cached_file_location) else: logger.info("No cached firmware file found. Downloading new version.") - response = requests.get("http://firmware.pyasic.org/latest") + async with httpx.AsyncClient() as client: + response = await client.get("http://firmware.pyasic.org/latest") response.raise_for_status() firmware_url = response.json().get("url") if not firmware_url: raise ValueError("Firmware URL not found in the server response.") - firmware_response = requests.get(firmware_url) + async with httpx.AsyncClient() as client: + firmware_response = await client.get(firmware_url) firmware_response.raise_for_status() - with open(cached_file_location, "wb") as firmware_file: + with cached_file_location.open("wb") as firmware_file: firmware_file.write(firmware_response.content) - file_location = cached_file_location + file_location = str(cached_file_location) # Upload the firmware file to the BOSMiner device logger.info(f"Uploading firmware file from {file_location} to the device.") From 0bd5c22681c18b4110d3eb085cc62d2ed5de0706 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Wed, 29 May 2024 08:54:13 +0530 Subject: [PATCH 06/10] Implement a class to handle firmware management tasks --- pyasic/miners/base.py | 119 +++++++++++++++++++++++++- pyasic/ssh/braiins_os.py | 87 ++++++++++++++----- tests/miners_tests/test_braiins_os.py | 24 ------ 3 files changed, 182 insertions(+), 48 deletions(-) delete mode 100644 tests/miners_tests/test_braiins_os.py diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index 4a8b5840..29699129 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -17,6 +17,10 @@ import asyncio import ipaddress import warnings from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union +from pathlib import Path +import re +import httpx +import hashlib from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard, MinerData @@ -523,6 +527,7 @@ class MinerProtocol(Protocol): class BaseMiner(MinerProtocol): def __init__(self, ip: str) -> None: self.ip = ip + self.firmware_manager = FirmwareManager("http://feeds.braiins-os.com") if self.expected_chips is None and self.raw_model is not None: warnings.warn( @@ -539,4 +544,116 @@ class BaseMiner(MinerProtocol): self.ssh = self._ssh_cls(ip) -AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) \ No newline at end of file +AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) + + +class FirmwareManager: + class FirmwareManager: + def __init__(self, remote_server_url: str): + """ + Initialize a FirmwareManager instance. + + Args: + remote_server_url (str): The URL of the remote server to fetch firmware information. + """ + self.remote_server_url = remote_server_url + self.version_extractors = {} + + # Register version extractor for braiins_os + self.register_version_extractor("braiins_os", self.extract_braiins_os_version) + + def extract_braiins_os_version(self, firmware_file: Path) -> str: + """ + Extract the firmware version from the filename for braiins_os miners. + + Args: + firmware_file (Path): The firmware file to extract the version from. + + Returns: + str: The extracted firmware version. + + Raises: + ValueError: If the version is not found in the filename. + """ + match = re.search(r"firmware_v(\d+\.\d+\.\d+)\.tar\.gz", firmware_file.name) + if match: + return match.group(1) + raise ValueError("Firmware version not found in the filename.") + + async def get_latest_firmware_info(self) -> dict: + """ + Fetch the latest firmware information from the remote server. + + Returns: + dict: The latest firmware information, including version and SHA256 hash. + + Raises: + httpx.HTTPStatusError: If the HTTP request fails. + """ + async with httpx.AsyncClient() as client: + response = await client.get(f"{self.remote_server_url}/latest") + response.raise_for_status() + return response.json() + + async def download_firmware(self, url: str, file_path: Path): + """ + Download the firmware file from the specified URL and save it to the given file path. + + Args: + url (str): The URL to download the firmware from. + file_path (Path): The file path to save the downloaded firmware. + + Raises: + httpx.HTTPStatusError: If the HTTP request fails. + """ + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + with file_path.open("wb") as firmware_file: + firmware_file.write(response.content) + + def calculate_sha256(self, file_path: Path) -> str: + """ + Calculate the SHA256 hash of the specified file. + + Args: + file_path (Path): The file path of the file to calculate the hash for. + + Returns: + str: The SHA256 hash of the file. + """ + sha256 = hashlib.sha256() + with file_path.open("rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256.update(chunk) + return sha256.hexdigest() + + def register_version_extractor(self, miner_type: str, extractor_func): + """ + Register a custom firmware version extraction function for a specific miner type. + + Args: + miner_type (str): The type of miner. + extractor_func (function): The function to extract the firmware version from the firmware file. + """ + self.version_extractors[miner_type] = extractor_func + + def get_firmware_version(self, miner_type: str, firmware_file: Path) -> str: + """ + Extract the firmware version from the firmware file using the registered extractor function for the miner type. + + Args: + miner_type (str): The type of miner. + firmware_file (Path): The firmware file to extract the version from. + + Returns: + str: The firmware version. + + Raises: + ValueError: If no extractor function is registered for the miner type or if the version is not found. + """ + if miner_type not in self.version_extractors: + raise ValueError(f"No version extractor registered for miner type: {miner_type}") + + extractor_func = self.version_extractors[miner_type] + return extractor_func(firmware_file) diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index c796adff..ca5827d6 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -4,7 +4,15 @@ from pyasic.ssh.base import BaseSSH import logging import httpx from pathlib import Path -import os +import hashlib +from pyasic.miners.base import FirmwareManager + +def calculate_sha256(file_path): + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256.update(chunk) + return sha256.hexdigest() # Set up logging logger = logging.getLogger(__name__) @@ -24,6 +32,27 @@ class BOSMinerSSH(BaseSSH): """ super().__init__(ip) self.pwd = settings.get("default_bosminer_ssh_password", "root") + self.firmware_manager = FirmwareManager() + + def get_firmware_version(self, firmware_file): + """ + Extract the firmware version from the firmware file. + + Args: + firmware_file (file): The firmware file to extract the version from. + + Returns: + str: The firmware version. + """ + import re + + # Extract the version from the filename using a regular expression + filename = firmware_file.name + match = re.search(r"firmware_v(\d+\.\d+\.\d+)\.tar\.gz", filename) + if match: + return match.group(1) + else: + raise ValueError("Firmware version not found in the filename.") async def get_board_info(self): """ @@ -106,12 +135,14 @@ class BOSMinerSSH(BaseSSH): """ return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off") - async def upgrade_firmware(self, file_location: str = None): + async def upgrade_firmware(self, file_location: str = None, custom_url: str = None, override_validation: bool = False): """ Upgrade the firmware of the BOSMiner device. Args: file_location (str): The local file path of the firmware to be uploaded. If not provided, the firmware will be downloaded from the internal server. + custom_url (str): Custom URL to download the firmware from. + override_validation (bool): Whether to override SHA256 validation. Returns: str: Confirmation message after upgrading the firmware. @@ -126,38 +157,36 @@ class BOSMinerSSH(BaseSSH): if cached_file_location.exists(): logger.info("Cached firmware file found. Checking version.") # Compare cached firmware version with the latest version on the server - async with httpx.AsyncClient() as client: - response = await client.get("http://firmware.pyasic.org/latest") - response.raise_for_status() - latest_version = response.json().get("version") - cached_version = self._get_fw_ver() + latest_firmware_info = await self.firmware_manager.get_latest_firmware_info() + latest_version = latest_firmware_info.get("version") + latest_hash = latest_firmware_info.get("sha256") + cached_version = self.firmware_manager.get_firmware_version("braiins_os", cached_file_location) if cached_version == latest_version: logger.info("Cached firmware version matches the latest version. Using cached file.") file_location = str(cached_file_location) else: logger.info("Cached firmware version does not match the latest version. Downloading new version.") - firmware_url = response.json().get("url") + firmware_url = custom_url or latest_firmware_info.get("url") if not firmware_url: raise ValueError("Firmware URL not found in the server response.") - async with httpx.AsyncClient() as client: - firmware_response = await client.get(firmware_url) - firmware_response.raise_for_status() - with cached_file_location.open("wb") as firmware_file: - firmware_file.write(firmware_response.content) + await self.firmware_manager.download_firmware(firmware_url, cached_file_location) + if not override_validation: + downloaded_hash = self.firmware_manager.calculate_sha256(cached_file_location) + if downloaded_hash != latest_hash: + raise ValueError("SHA256 hash validation failed for the downloaded firmware file.") file_location = str(cached_file_location) else: logger.info("No cached firmware file found. Downloading new version.") - async with httpx.AsyncClient() as client: - response = await client.get("http://firmware.pyasic.org/latest") - response.raise_for_status() - firmware_url = response.json().get("url") + latest_firmware_info = await self.firmware_manager.get_latest_firmware_info() + firmware_url = custom_url or latest_firmware_info.get("url") + latest_hash = latest_firmware_info.get("sha256") if not firmware_url: raise ValueError("Firmware URL not found in the server response.") - async with httpx.AsyncClient() as client: - firmware_response = await client.get(firmware_url) - firmware_response.raise_for_status() - with cached_file_location.open("wb") as firmware_file: - firmware_file.write(firmware_response.content) + await self.firmware_manager.download_firmware(firmware_url, cached_file_location) + if not override_validation: + downloaded_hash = self.firmware_manager.calculate_sha256(cached_file_location) + if downloaded_hash != latest_hash: + raise ValueError("SHA256 hash validation failed for the downloaded firmware file.") file_location = str(cached_file_location) # Upload the firmware file to the BOSMiner device @@ -174,6 +203,18 @@ class BOSMinerSSH(BaseSSH): logger.info("Firmware upgrade process completed successfully.") return result + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred during the firmware upgrade process: {e}") + raise + except FileNotFoundError as e: + logger.error(f"File not found during the firmware upgrade process: {e}") + raise + except ValueError as e: + logger.error(f"Validation error occurred during the firmware upgrade process: {e}") + raise + except OSError as e: + logger.error(f"OS error occurred during the firmware upgrade process: {e}") + raise except Exception as e: - logger.error(f"An error occurred during the firmware upgrade process: {e}") + logger.error(f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True) raise \ No newline at end of file diff --git a/tests/miners_tests/test_braiins_os.py b/tests/miners_tests/test_braiins_os.py deleted file mode 100644 index b3a4eb1e..00000000 --- a/tests/miners_tests/test_braiins_os.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest -from unittest.mock import patch, mock_open -from pyasic.ssh.braiins_os import BOSMinerSSH - -@pytest.fixture -def bosminer_ssh(): - return BOSMinerSSH(ip="192.168.1.100") - -@pytest.mark.asyncio -async def test_upgrade_firmware_with_valid_file_location(bosminer_ssh): - with patch("pyasic.ssh.braiins_os.os.path.exists") as mock_exists, \ - patch("pyasic.ssh.braiins_os.open", mock_open(read_data="data")) as mock_file, \ - patch("pyasic.ssh.braiins_os.requests.get") as mock_get, \ - patch.object(bosminer_ssh, "send_command") as mock_send_command: - - mock_exists.return_value = False - file_location = "/path/to/firmware.tar.gz" - - result = await bosminer_ssh.upgrade_firmware(file_location=file_location) - - mock_send_command.assert_any_call(f"scp {file_location} root@{bosminer_ssh.ip}:/tmp/firmware.tar.gz") - mock_send_command.assert_any_call("tar -xzf /tmp/firmware.tar.gz -C /tmp") - mock_send_command.assert_any_call("sh /tmp/upgrade_firmware.sh") - assert result is not None \ No newline at end of file From 26d9562c181c1fdfd4da870df698e7ecd717c3a9 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Wed, 29 May 2024 17:52:01 +0530 Subject: [PATCH 07/10] Add structured directories for firmware update --- pyasic/miners/base.py | 119 +-------------------------------------- pyasic/ssh/braiins_os.py | 52 ++--------------- pyasic/updater/bos.py | 111 ++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 164 deletions(-) create mode 100644 pyasic/updater/bos.py diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index 29699129..4a8b5840 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -17,10 +17,6 @@ import asyncio import ipaddress import warnings from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union -from pathlib import Path -import re -import httpx -import hashlib from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard, MinerData @@ -527,7 +523,6 @@ class MinerProtocol(Protocol): class BaseMiner(MinerProtocol): def __init__(self, ip: str) -> None: self.ip = ip - self.firmware_manager = FirmwareManager("http://feeds.braiins-os.com") if self.expected_chips is None and self.raw_model is not None: warnings.warn( @@ -544,116 +539,4 @@ class BaseMiner(MinerProtocol): self.ssh = self._ssh_cls(ip) -AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) - - -class FirmwareManager: - class FirmwareManager: - def __init__(self, remote_server_url: str): - """ - Initialize a FirmwareManager instance. - - Args: - remote_server_url (str): The URL of the remote server to fetch firmware information. - """ - self.remote_server_url = remote_server_url - self.version_extractors = {} - - # Register version extractor for braiins_os - self.register_version_extractor("braiins_os", self.extract_braiins_os_version) - - def extract_braiins_os_version(self, firmware_file: Path) -> str: - """ - Extract the firmware version from the filename for braiins_os miners. - - Args: - firmware_file (Path): The firmware file to extract the version from. - - Returns: - str: The extracted firmware version. - - Raises: - ValueError: If the version is not found in the filename. - """ - match = re.search(r"firmware_v(\d+\.\d+\.\d+)\.tar\.gz", firmware_file.name) - if match: - return match.group(1) - raise ValueError("Firmware version not found in the filename.") - - async def get_latest_firmware_info(self) -> dict: - """ - Fetch the latest firmware information from the remote server. - - Returns: - dict: The latest firmware information, including version and SHA256 hash. - - Raises: - httpx.HTTPStatusError: If the HTTP request fails. - """ - async with httpx.AsyncClient() as client: - response = await client.get(f"{self.remote_server_url}/latest") - response.raise_for_status() - return response.json() - - async def download_firmware(self, url: str, file_path: Path): - """ - Download the firmware file from the specified URL and save it to the given file path. - - Args: - url (str): The URL to download the firmware from. - file_path (Path): The file path to save the downloaded firmware. - - Raises: - httpx.HTTPStatusError: If the HTTP request fails. - """ - async with httpx.AsyncClient() as client: - response = await client.get(url) - response.raise_for_status() - with file_path.open("wb") as firmware_file: - firmware_file.write(response.content) - - def calculate_sha256(self, file_path: Path) -> str: - """ - Calculate the SHA256 hash of the specified file. - - Args: - file_path (Path): The file path of the file to calculate the hash for. - - Returns: - str: The SHA256 hash of the file. - """ - sha256 = hashlib.sha256() - with file_path.open("rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - sha256.update(chunk) - return sha256.hexdigest() - - def register_version_extractor(self, miner_type: str, extractor_func): - """ - Register a custom firmware version extraction function for a specific miner type. - - Args: - miner_type (str): The type of miner. - extractor_func (function): The function to extract the firmware version from the firmware file. - """ - self.version_extractors[miner_type] = extractor_func - - def get_firmware_version(self, miner_type: str, firmware_file: Path) -> str: - """ - Extract the firmware version from the firmware file using the registered extractor function for the miner type. - - Args: - miner_type (str): The type of miner. - firmware_file (Path): The firmware file to extract the version from. - - Returns: - str: The firmware version. - - Raises: - ValueError: If no extractor function is registered for the miner type or if the version is not found. - """ - if miner_type not in self.version_extractors: - raise ValueError(f"No version extractor registered for miner type: {miner_type}") - - extractor_func = self.version_extractors[miner_type] - return extractor_func(firmware_file) +AnyMiner = TypeVar("AnyMiner", bound=BaseMiner) \ No newline at end of file diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index ca5827d6..618c0e3c 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -4,8 +4,9 @@ from pyasic.ssh.base import BaseSSH import logging import httpx from pathlib import Path +import os import hashlib -from pyasic.miners.base import FirmwareManager +from pyasic.updater.bos import FirmwareManager def calculate_sha256(file_path): sha256 = hashlib.sha256() @@ -135,14 +136,12 @@ class BOSMinerSSH(BaseSSH): """ return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off") - async def upgrade_firmware(self, file_location: str = None, custom_url: str = None, override_validation: bool = False): + async def upgrade_firmware(self, file_location: str): """ Upgrade the firmware of the BOSMiner device. Args: - file_location (str): The local file path of the firmware to be uploaded. If not provided, the firmware will be downloaded from the internal server. - custom_url (str): Custom URL to download the firmware from. - override_validation (bool): Whether to override SHA256 validation. + file_location (str): The local file path of the firmware to be uploaded. Returns: str: Confirmation message after upgrading the firmware. @@ -150,44 +149,8 @@ class BOSMinerSSH(BaseSSH): try: logger.info("Starting firmware upgrade process.") - if file_location is None: - # Check for cached firmware file - with tempfile.NamedTemporaryFile(delete=False) as temp_firmware_file: - cached_file_location = Path(temp_firmware_file.name) - if cached_file_location.exists(): - logger.info("Cached firmware file found. Checking version.") - # Compare cached firmware version with the latest version on the server - latest_firmware_info = await self.firmware_manager.get_latest_firmware_info() - latest_version = latest_firmware_info.get("version") - latest_hash = latest_firmware_info.get("sha256") - cached_version = self.firmware_manager.get_firmware_version("braiins_os", cached_file_location) - if cached_version == latest_version: - logger.info("Cached firmware version matches the latest version. Using cached file.") - file_location = str(cached_file_location) - else: - logger.info("Cached firmware version does not match the latest version. Downloading new version.") - firmware_url = custom_url or latest_firmware_info.get("url") - if not firmware_url: - raise ValueError("Firmware URL not found in the server response.") - await self.firmware_manager.download_firmware(firmware_url, cached_file_location) - if not override_validation: - downloaded_hash = self.firmware_manager.calculate_sha256(cached_file_location) - if downloaded_hash != latest_hash: - raise ValueError("SHA256 hash validation failed for the downloaded firmware file.") - file_location = str(cached_file_location) - else: - logger.info("No cached firmware file found. Downloading new version.") - latest_firmware_info = await self.firmware_manager.get_latest_firmware_info() - firmware_url = custom_url or latest_firmware_info.get("url") - latest_hash = latest_firmware_info.get("sha256") - if not firmware_url: - raise ValueError("Firmware URL not found in the server response.") - await self.firmware_manager.download_firmware(firmware_url, cached_file_location) - if not override_validation: - downloaded_hash = self.firmware_manager.calculate_sha256(cached_file_location) - if downloaded_hash != latest_hash: - raise ValueError("SHA256 hash validation failed for the downloaded firmware file.") - file_location = str(cached_file_location) + if not file_location: + raise ValueError("File location must be provided for firmware upgrade.") # Upload the firmware file to the BOSMiner device logger.info(f"Uploading firmware file from {file_location} to the device.") @@ -203,9 +166,6 @@ class BOSMinerSSH(BaseSSH): logger.info("Firmware upgrade process completed successfully.") return result - except httpx.HTTPStatusError as e: - logger.error(f"HTTP error occurred during the firmware upgrade process: {e}") - raise except FileNotFoundError as e: logger.error(f"File not found during the firmware upgrade process: {e}") raise diff --git a/pyasic/updater/bos.py b/pyasic/updater/bos.py new file mode 100644 index 00000000..75f84dc8 --- /dev/null +++ b/pyasic/updater/bos.py @@ -0,0 +1,111 @@ +import httpx +from pathlib import Path +import re +import hashlib + +class FirmwareManager: + def __init__(self): + """ + Initialize a FirmwareManager instance. + """ + self.remote_server_url = "http://feeds.braiins-os.com" + self.version_extractors = {} + + # Register version extractor for braiins_os + self.register_version_extractor("braiins_os", self.extract_braiins_os_version) + + def extract_braiins_os_version(self, firmware_file: Path) -> str: + """ + Extract the firmware version from the filename for braiins_os miners. + + Args: + firmware_file (Path): The firmware file to extract the version from. + + Returns: + str: The extracted firmware version. + + Raises: + ValueError: If the version is not found in the filename. + """ + match = re.search(r"(am1_s9|am2_x17|am3_bbb)/firmware_v(\d+\.\d+\.\d+)\.tar", firmware_file.name) + if match: + return match.group(2) + raise ValueError("Firmware version not found in the filename.") + + async def get_latest_firmware_info(self) -> dict: + """ + Fetch the latest firmware information from the remote server. + + Returns: + dict: The latest firmware information, including version and SHA256 hash. + + Raises: + httpx.HTTPStatusError: If the HTTP request fails. + """ + async with httpx.AsyncClient() as client: + response = await client.get(f"{self.remote_server_url}/latest") + response.raise_for_status() + return response.json() + + async def download_firmware(self, url: str, file_path: Path): + """ + Download the firmware file from the specified URL and save it to the given file path. + + Args: + url (str): The URL to download the firmware from. + file_path (Path): The file path to save the downloaded firmware. + + Raises: + httpx.HTTPStatusError: If the HTTP request fails. + """ + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + with file_path.open("wb") as firmware_file: + firmware_file.write(response.content) + + def calculate_sha256(self, file_path: Path) -> str: + """ + Calculate the SHA256 hash of the specified file. + + Args: + file_path (Path): The file path of the file to calculate the hash for. + + Returns: + str: The SHA256 hash of the file. + """ + sha256 = hashlib.sha256() + with file_path.open("rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256.update(chunk) + return sha256.hexdigest() + + def register_version_extractor(self, miner_type: str, extractor_func): + """ + Register a custom firmware version extraction function for a specific miner type. + + Args: + miner_type (str): The type of miner. + extractor_func (function): The function to extract the firmware version from the firmware file. + """ + self.version_extractors[miner_type] = extractor_func + + def get_firmware_version(self, miner_type: str, firmware_file: Path) -> str: + """ + Extract the firmware version from the firmware file using the registered extractor function for the miner type. + + Args: + miner_type (str): The type of miner. + firmware_file (Path): The firmware file to extract the version from. + + Returns: + str: The firmware version. + + Raises: + ValueError: If no extractor function is registered for the miner type or if the version is not found. + """ + if miner_type not in self.version_extractors: + raise ValueError(f"No version extractor registered for miner type: {miner_type}") + + extractor_func = self.version_extractors[miner_type] + return extractor_func(firmware_file) \ No newline at end of file From b4faf7c49ee6ab575a66ddde3f2c7ebbd00a2434 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Wed, 29 May 2024 18:02:43 +0530 Subject: [PATCH 08/10] Fix code style issues --- pyasic/ssh/braiins_os.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index 618c0e3c..a3cc0fbf 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -1,10 +1,6 @@ -import tempfile from pyasic import settings from pyasic.ssh.base import BaseSSH import logging -import httpx -from pathlib import Path -import os import hashlib from pyasic.updater.bos import FirmwareManager @@ -23,6 +19,7 @@ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(messag handler.setFormatter(formatter) logger.addHandler(handler) + class BOSMinerSSH(BaseSSH): def __init__(self, ip: str): """ From 7688288d0583c84d764f5bba72433038ee1df791 Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Mon, 3 Jun 2024 02:46:34 +0530 Subject: [PATCH 09/10] Improved file structure and add requested changes --- pyasic/miners/backends/braiins_os.py | 45 +++++++++++++++ pyasic/ssh/braiins_os.py | 84 +--------------------------- pyasic/updater/__init__.py | 42 ++++++++++++++ pyasic/updater/bos.py | 61 +++++++++++++++++++- 4 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 pyasic/updater/__init__.py diff --git a/pyasic/miners/backends/braiins_os.py b/pyasic/miners/backends/braiins_os.py index 00d86266..6be97533 100644 --- a/pyasic/miners/backends/braiins_os.py +++ b/pyasic/miners/backends/braiins_os.py @@ -37,6 +37,9 @@ from pyasic.ssh.braiins_os import BOSMinerSSH from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI from pyasic.web.braiins_os.proto.braiins.bos.v1 import SaveAction +import aiofiles +import base64 + BOSMINER_DATA_LOC = DataLocations( **{ str(DataOptions.MAC): DataFunction( @@ -570,6 +573,48 @@ class BOSMiner(BraiinsOSFirmware): except LookupError: pass + async def upgrade_firmware(self, file: Path): + """ + Upgrade the firmware of the BOSMiner device. + + Args: + file (Path): The local file path of the firmware to be uploaded. + + Returns: + str: Confirmation message after upgrading the firmware. + """ + try: + self.logger.info("Starting firmware upgrade process.") + + if not file: + raise ValueError("File location must be provided for firmware upgrade.") + + # Read the firmware file contents + async with aiofiles.open(file, "rb") as f: + upgrade_contents = await f.read() + + # Encode the firmware contents in base64 + encoded_contents = base64.b64encode(upgrade_contents).decode('utf-8') + + # Upload the firmware file to the BOSMiner device + self.logger.info(f"Uploading firmware file from {file} to the device.") + await self.ssh.send_command(f"echo {encoded_contents} | base64 -d > /tmp/firmware.tar && sysupgrade /tmp/firmware.tar") + + self.logger.info("Firmware upgrade process completed successfully.") + return "Firmware upgrade completed successfully." + except FileNotFoundError as e: + self.logger.error(f"File not found during the firmware upgrade process: {e}") + raise + except ValueError as e: + self.logger.error(f"Validation error occurred during the firmware upgrade process: {e}") + raise + except OSError as e: + self.logger.error(f"OS error occurred during the firmware upgrade process: {e}") + raise + except Exception as e: + self.logger.error(f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True) + raise + BOSER_DATA_LOC = DataLocations( **{ diff --git a/pyasic/ssh/braiins_os.py b/pyasic/ssh/braiins_os.py index a3cc0fbf..28ad000d 100644 --- a/pyasic/ssh/braiins_os.py +++ b/pyasic/ssh/braiins_os.py @@ -1,23 +1,5 @@ from pyasic import settings from pyasic.ssh.base import BaseSSH -import logging -import hashlib -from pyasic.updater.bos import FirmwareManager - -def calculate_sha256(file_path): - sha256 = hashlib.sha256() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - sha256.update(chunk) - return sha256.hexdigest() - -# Set up logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) -handler = logging.StreamHandler() -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -handler.setFormatter(formatter) -logger.addHandler(handler) class BOSMinerSSH(BaseSSH): @@ -30,27 +12,6 @@ class BOSMinerSSH(BaseSSH): """ super().__init__(ip) self.pwd = settings.get("default_bosminer_ssh_password", "root") - self.firmware_manager = FirmwareManager() - - def get_firmware_version(self, firmware_file): - """ - Extract the firmware version from the firmware file. - - Args: - firmware_file (file): The firmware file to extract the version from. - - Returns: - str: The firmware version. - """ - import re - - # Extract the version from the filename using a regular expression - filename = firmware_file.name - match = re.search(r"firmware_v(\d+\.\d+\.\d+)\.tar\.gz", filename) - if match: - return match.group(1) - else: - raise ValueError("Firmware version not found in the filename.") async def get_board_info(self): """ @@ -131,47 +92,4 @@ class BOSMinerSSH(BaseSSH): Returns: str: Status of the LED. """ - return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off") - - async def upgrade_firmware(self, file_location: str): - """ - Upgrade the firmware of the BOSMiner device. - - Args: - file_location (str): The local file path of the firmware to be uploaded. - - Returns: - str: Confirmation message after upgrading the firmware. - """ - try: - logger.info("Starting firmware upgrade process.") - - if not file_location: - raise ValueError("File location must be provided for firmware upgrade.") - - # Upload the firmware file to the BOSMiner device - logger.info(f"Uploading firmware file from {file_location} to the device.") - await self.send_command(f"scp {file_location} root@{self.ip}:/tmp/firmware.tar.gz") - - # Extract the firmware file - logger.info("Extracting the firmware file on the device.") - await self.send_command("tar -xzf /tmp/firmware.tar.gz -C /tmp") - - # Run the firmware upgrade script - logger.info("Running the firmware upgrade script on the device.") - result = await self.send_command("sh /tmp/upgrade_firmware.sh") - - logger.info("Firmware upgrade process completed successfully.") - return result - except FileNotFoundError as e: - logger.error(f"File not found during the firmware upgrade process: {e}") - raise - except ValueError as e: - logger.error(f"Validation error occurred during the firmware upgrade process: {e}") - raise - except OSError as e: - logger.error(f"OS error occurred during the firmware upgrade process: {e}") - raise - except Exception as e: - logger.error(f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True) - raise \ No newline at end of file + return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off") \ No newline at end of file diff --git a/pyasic/updater/__init__.py b/pyasic/updater/__init__.py new file mode 100644 index 00000000..21d4ec31 --- /dev/null +++ b/pyasic/updater/__init__.py @@ -0,0 +1,42 @@ +from pathlib import Path +from pyasic.updater.bos import FirmwareManager + +class FirmwareUpdater: + def __init__(self, firmware, raw_model, ssh_client): + """ + Initialize a FirmwareUpdater instance. + + Args: + firmware: The firmware type of the miner. + raw_model: The raw model of the miner. + ssh_client: The SSH client to use for sending commands to the device. + """ + self.firmware = firmware + self.raw_model = raw_model + self.ssh_client = ssh_client + self.manager = self._get_manager() + + def _get_manager(self): + """ + Get the appropriate firmware manager based on the firmware type and raw model. + + Returns: + The firmware manager instance. + """ + if self.firmware == "braiins_os": + return FirmwareManager(self.ssh_client) + # Add more conditions here for different firmware types and raw models + else: + raise ValueError(f"Unsupported firmware type: {self.firmware}") + + async def upgrade_firmware(self, file: Path): + """ + Upgrade the firmware of the miner. + + Args: + file (Path): The local file path of the firmware to be uploaded. + + Returns: + str: Confirmation message after upgrading the firmware. + """ + return await self.manager.upgrade_firmware(file) diff --git a/pyasic/updater/bos.py b/pyasic/updater/bos.py index 75f84dc8..c46f10d6 100644 --- a/pyasic/updater/bos.py +++ b/pyasic/updater/bos.py @@ -2,14 +2,28 @@ import httpx from pathlib import Path import re import hashlib +import aiofiles +import logging class FirmwareManager: - def __init__(self): + def __init__(self, ssh_client): """ Initialize a FirmwareManager instance. + + Args: + ssh_client: The SSH client to use for sending commands to the device. """ self.remote_server_url = "http://feeds.braiins-os.com" self.version_extractors = {} + self.ssh = ssh_client + + # Set up logging + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) # Register version extractor for braiins_os self.register_version_extractor("braiins_os", self.extract_braiins_os_version) @@ -108,4 +122,47 @@ class FirmwareManager: raise ValueError(f"No version extractor registered for miner type: {miner_type}") extractor_func = self.version_extractors[miner_type] - return extractor_func(firmware_file) \ No newline at end of file + return extractor_func(firmware_file) + + async def upgrade_firmware(self, file: Path): + """ + Upgrade the firmware of the BOSMiner device. + + Args: + file (Path): The local file path of the firmware to be uploaded. + + Returns: + str: Confirmation message after upgrading the firmware. + """ + try: + self.logger.info("Starting firmware upgrade process.") + + if not file: + raise ValueError("File location must be provided for firmware upgrade.") + + # Read the firmware file contents + async with aiofiles.open(file, "rb") as f: + upgrade_contents = await f.read() + + # Encode the firmware contents in base64 + import base64 + encoded_contents = base64.b64encode(upgrade_contents).decode('utf-8') + + # Upload the firmware file to the BOSMiner device + self.logger.info(f"Uploading firmware file from {file} to the device.") + await self.ssh.send_command(f"echo {encoded_contents} | base64 -d > /tmp/firmware.tar && sysupgrade /tmp/firmware.tar") + + self.logger.info("Firmware upgrade process completed successfully.") + return "Firmware upgrade completed successfully." + except FileNotFoundError as e: + self.logger.error(f"File not found during the firmware upgrade process: {e}") + raise + except ValueError as e: + self.logger.error(f"Validation error occurred during the firmware upgrade process: {e}") + raise + except OSError as e: + self.logger.error(f"OS error occurred during the firmware upgrade process: {e}") + raise + except Exception as e: + self.logger.error(f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True) + raise From c87880529ca0574eca32b4336064bf256c00789a Mon Sep 17 00:00:00 2001 From: 1e9abhi1e10 <2311abhiptdr@gmail.com> Date: Wed, 5 Jun 2024 06:35:08 +0530 Subject: [PATCH 10/10] Removed updater directory --- pyasic/updater/__init__.py | 42 ---------- pyasic/updater/bos.py | 168 ------------------------------------- 2 files changed, 210 deletions(-) delete mode 100644 pyasic/updater/__init__.py delete mode 100644 pyasic/updater/bos.py diff --git a/pyasic/updater/__init__.py b/pyasic/updater/__init__.py deleted file mode 100644 index 21d4ec31..00000000 --- a/pyasic/updater/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path -from pyasic.updater.bos import FirmwareManager - -class FirmwareUpdater: - def __init__(self, firmware, raw_model, ssh_client): - """ - Initialize a FirmwareUpdater instance. - - Args: - firmware: The firmware type of the miner. - raw_model: The raw model of the miner. - ssh_client: The SSH client to use for sending commands to the device. - """ - self.firmware = firmware - self.raw_model = raw_model - self.ssh_client = ssh_client - self.manager = self._get_manager() - - def _get_manager(self): - """ - Get the appropriate firmware manager based on the firmware type and raw model. - - Returns: - The firmware manager instance. - """ - if self.firmware == "braiins_os": - return FirmwareManager(self.ssh_client) - # Add more conditions here for different firmware types and raw models - else: - raise ValueError(f"Unsupported firmware type: {self.firmware}") - - async def upgrade_firmware(self, file: Path): - """ - Upgrade the firmware of the miner. - - Args: - file (Path): The local file path of the firmware to be uploaded. - - Returns: - str: Confirmation message after upgrading the firmware. - """ - return await self.manager.upgrade_firmware(file) diff --git a/pyasic/updater/bos.py b/pyasic/updater/bos.py deleted file mode 100644 index c46f10d6..00000000 --- a/pyasic/updater/bos.py +++ /dev/null @@ -1,168 +0,0 @@ -import httpx -from pathlib import Path -import re -import hashlib -import aiofiles -import logging - -class FirmwareManager: - def __init__(self, ssh_client): - """ - Initialize a FirmwareManager instance. - - Args: - ssh_client: The SSH client to use for sending commands to the device. - """ - self.remote_server_url = "http://feeds.braiins-os.com" - self.version_extractors = {} - self.ssh = ssh_client - - # Set up logging - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - # Register version extractor for braiins_os - self.register_version_extractor("braiins_os", self.extract_braiins_os_version) - - def extract_braiins_os_version(self, firmware_file: Path) -> str: - """ - Extract the firmware version from the filename for braiins_os miners. - - Args: - firmware_file (Path): The firmware file to extract the version from. - - Returns: - str: The extracted firmware version. - - Raises: - ValueError: If the version is not found in the filename. - """ - match = re.search(r"(am1_s9|am2_x17|am3_bbb)/firmware_v(\d+\.\d+\.\d+)\.tar", firmware_file.name) - if match: - return match.group(2) - raise ValueError("Firmware version not found in the filename.") - - async def get_latest_firmware_info(self) -> dict: - """ - Fetch the latest firmware information from the remote server. - - Returns: - dict: The latest firmware information, including version and SHA256 hash. - - Raises: - httpx.HTTPStatusError: If the HTTP request fails. - """ - async with httpx.AsyncClient() as client: - response = await client.get(f"{self.remote_server_url}/latest") - response.raise_for_status() - return response.json() - - async def download_firmware(self, url: str, file_path: Path): - """ - Download the firmware file from the specified URL and save it to the given file path. - - Args: - url (str): The URL to download the firmware from. - file_path (Path): The file path to save the downloaded firmware. - - Raises: - httpx.HTTPStatusError: If the HTTP request fails. - """ - async with httpx.AsyncClient() as client: - response = await client.get(url) - response.raise_for_status() - with file_path.open("wb") as firmware_file: - firmware_file.write(response.content) - - def calculate_sha256(self, file_path: Path) -> str: - """ - Calculate the SHA256 hash of the specified file. - - Args: - file_path (Path): The file path of the file to calculate the hash for. - - Returns: - str: The SHA256 hash of the file. - """ - sha256 = hashlib.sha256() - with file_path.open("rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - sha256.update(chunk) - return sha256.hexdigest() - - def register_version_extractor(self, miner_type: str, extractor_func): - """ - Register a custom firmware version extraction function for a specific miner type. - - Args: - miner_type (str): The type of miner. - extractor_func (function): The function to extract the firmware version from the firmware file. - """ - self.version_extractors[miner_type] = extractor_func - - def get_firmware_version(self, miner_type: str, firmware_file: Path) -> str: - """ - Extract the firmware version from the firmware file using the registered extractor function for the miner type. - - Args: - miner_type (str): The type of miner. - firmware_file (Path): The firmware file to extract the version from. - - Returns: - str: The firmware version. - - Raises: - ValueError: If no extractor function is registered for the miner type or if the version is not found. - """ - if miner_type not in self.version_extractors: - raise ValueError(f"No version extractor registered for miner type: {miner_type}") - - extractor_func = self.version_extractors[miner_type] - return extractor_func(firmware_file) - - async def upgrade_firmware(self, file: Path): - """ - Upgrade the firmware of the BOSMiner device. - - Args: - file (Path): The local file path of the firmware to be uploaded. - - Returns: - str: Confirmation message after upgrading the firmware. - """ - try: - self.logger.info("Starting firmware upgrade process.") - - if not file: - raise ValueError("File location must be provided for firmware upgrade.") - - # Read the firmware file contents - async with aiofiles.open(file, "rb") as f: - upgrade_contents = await f.read() - - # Encode the firmware contents in base64 - import base64 - encoded_contents = base64.b64encode(upgrade_contents).decode('utf-8') - - # Upload the firmware file to the BOSMiner device - self.logger.info(f"Uploading firmware file from {file} to the device.") - await self.ssh.send_command(f"echo {encoded_contents} | base64 -d > /tmp/firmware.tar && sysupgrade /tmp/firmware.tar") - - self.logger.info("Firmware upgrade process completed successfully.") - return "Firmware upgrade completed successfully." - except FileNotFoundError as e: - self.logger.error(f"File not found during the firmware upgrade process: {e}") - raise - except ValueError as e: - self.logger.error(f"Validation error occurred during the firmware upgrade process: {e}") - raise - except OSError as e: - self.logger.error(f"OS error occurred during the firmware upgrade process: {e}") - raise - except Exception as e: - self.logger.error(f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True) - raise