def get_geth_available_version(): # Get the available version for Geth, potentially for update log.info('Getting Geth available version...') subprocess.run(['apt', '-y', 'update']) process_result = subprocess.run(['apt-cache', 'policy', 'geth'], capture_output=True, text=True) if process_result.returncode != 0: log.error(f'Unexpected return code from apt-cache. Return code: ' f'{process_result.returncode}') return UNKNOWN_VALUE process_output = process_result.stdout result = re.search(r'Candidate: (?P<version>[^\+]+)', process_output) if not result: log.error(f'Cannot parse {process_output} for Geth candidate version.') return UNKNOWN_VALUE available_version = result.group('version') log.info(f'Geth available version is {available_version}') return available_version
def get_lighthouse_installed_version(): # Get the installed version for Lighthouse log.info('Getting Lighthouse installed version...') process_result = subprocess.run([LIGHTHOUSE_INSTALLED_PATH, '--version'], capture_output=True, text=True) if process_result.returncode != 0: log.error(f'Unexpected return code from Lighthouse. Return code: ' f'{process_result.returncode}') return UNKNOWN_VALUE process_output = process_result.stdout result = re.search(r'Lighthouse v?(?P<version>[^-]+)', process_output) if not result: log.error( f'Cannot parse {process_output} for Lighthouse installed version.') return UNKNOWN_VALUE installed_version = result.group('version') log.info(f'Lighthouse installed version is {installed_version}') return installed_version
def get_geth_installed_version(): # Get the installed version for Geth log.info('Getting Geth installed version...') process_result = subprocess.run(['geth', 'version'], capture_output=True, text=True) if process_result.returncode != 0: log.error(f'Unexpected return code from geth. Return code: ' f'{process_result.returncode}') return UNKNOWN_VALUE process_output = process_result.stdout result = re.search(r'Version: (?P<version>[^-]+)', process_output) if not result: log.error(f'Cannot parse {process_output} for Geth installed version.') return UNKNOWN_VALUE installed_version = result.group('version') log.info(f'Geth installed version is {installed_version}') return installed_version
def get_execution_client_details(execution_client): # Get the details for the current execution client if execution_client == EXECUTION_CLIENT_GETH: details = { 'service': { 'found': False, 'load': UNKNOWN_VALUE, 'active': UNKNOWN_VALUE, 'sub': UNKNOWN_VALUE }, 'versions': { 'installed': UNKNOWN_VALUE, 'running': UNKNOWN_VALUE, 'available': UNKNOWN_VALUE, 'latest': UNKNOWN_VALUE } } # Check for existing systemd service geth_service_exists = False geth_service_name = GETH_SYSTEMD_SERVICE_NAME service_details = get_systemd_service_details(geth_service_name) if service_details['LoadState'] == 'loaded': geth_service_exists = True if not geth_service_exists: return details details['service']['found'] = True details['service']['load'] = service_details['LoadState'] details['service']['active'] = service_details['ActiveState'] details['service']['sub'] = service_details['SubState'] details['service']['running'] = is_service_running(service_details) details['versions']['installed'] = get_geth_installed_version() details['versions']['running'] = get_geth_running_version() details['versions']['available'] = get_geth_available_version() details['versions']['latest'] = get_geth_latest_version() return details else: log.error(f'Unknown execution client {execution_client}.') return False
def enter_maintenance(context): # Maintenance entry point for Ubuntu. # Maintenance is started after the wizard has completed. log.info(f'Entering maintenance mode. To be implemented.') if context is None: log.error('Missing context.') return False context = use_default_client(context) if context is None: log.error('Missing context.') return False return show_dashboard(context)
def get_geth_running_version(): # Get the running version for Geth log.info('Getting Geth running version...') local_geth_jsonrpc_url = 'http://127.0.0.1:8545' request_json = {'jsonrpc': '2.0', 'method': 'web3_clientVersion', 'id': 67} headers = {'Content-Type': 'application/json'} try: response = httpx.post(local_geth_jsonrpc_url, json=request_json, headers=headers) except httpx.RequestError as exception: log.error(f'Cannot connect to Geth. Exception: {exception}') return UNKNOWN_VALUE if response.status_code != 200: log.error( f'Unexpected status code from {local_geth_jsonrpc_url}. Status code: ' f'{response.status_code}') return UNKNOWN_VALUE response_json = response.json() if 'result' not in response_json: log.error( f'Unexpected JSON response from {local_geth_jsonrpc_url}. result not found.' ) return UNKNOWN_VALUE version_agent = response_json['result'] # Version agent should look like: Geth/v1.10.12-stable-6c4dc6c3/linux-amd64/go1.17.2 result = re.search( r'Geth/v(?P<version>[^-/]+)(-(?P<stable>[^-/]+))?(-(?P<commit>[^-/]+))?', version_agent) if not result: log.error(f'Cannot parse {version_agent} for Geth version.') return UNKNOWN_VALUE running_version = result.group('version') log.info(f'Geth running version is {running_version}') return running_version
def get_lighthouse_latest_version(): # Get the latest version for Lighthouse log.info('Getting Lighthouse latest version...') lighthouse_gh_release_url = GITHUB_REST_API_URL + LIGHTHOUSE_LATEST_RELEASE headers = {'Accept': GITHUB_API_VERSION} try: response = httpx.get(lighthouse_gh_release_url, headers=headers, follow_redirects=True) except httpx.RequestError as exception: log.error( f'Exception while getting the latest stable version for Lighthouse. {exception}' ) return UNKNOWN_VALUE if response.status_code != 200: log.error( f'HTTP error while getting the latest stable version for Lighthouse. ' f'Status code {response.status_code}') return UNKNOWN_VALUE release_json = response.json() if 'tag_name' not in release_json or not isinstance( release_json['tag_name'], str): log.error( f'Unable to find tag name in Github response while getting the latest stable ' f'version for Lighthouse.') return UNKNOWN_VALUE tag_name = release_json['tag_name'] result = re.search(r'v?(?P<version>.+)', tag_name) if not result: log.error(f'Cannot parse tag name {tag_name} for Lighthouse version.') return UNKNOWN_VALUE latest_version = result.group('version') log.info(f'Lighthouse latest version is {latest_version}') return latest_version
def get_lighthouse_running_version(): # Get the running version for Lighthouse log.info('Getting Lighthouse running version...') local_lighthouse_bn_version_url = 'http://127.0.0.1:5052' + BN_VERSION_EP try: response = httpx.get(local_lighthouse_bn_version_url) except httpx.RequestError as exception: log.error(f'Cannot connect to Lighthouse. Exception: {exception}') return UNKNOWN_VALUE if response.status_code != 200: log.error( f'Unexpected status code from {local_lighthouse_bn_version_url}. Status code: ' f'{response.status_code}') return UNKNOWN_VALUE response_json = response.json() if 'data' not in response_json or 'version' not in response_json['data']: log.error( f'Unexpected JSON response from {local_lighthouse_bn_version_url}. result not found.' ) return UNKNOWN_VALUE version_agent = response_json['data']['version'] # Version agent should look like: Lighthouse/v2.0.1-aaa5344/x86_64-linux result = re.search( r'Lighthouse/v(?P<version>[^-/]+)(-(?P<commit>[^-/]+))?', version_agent) if not result: log.error(f'Cannot parse {version_agent} for Lighthouse version.') return UNKNOWN_VALUE running_version = result.group('version') log.info(f'Lighthouse running version is {running_version}') return running_version
def upgrade_lighthouse(): # Upgrade the Lighthouse client log.info('Upgrading Lighthouse client...') # Getting latest Lighthouse release files lighthouse_gh_release_url = GITHUB_REST_API_URL + LIGHTHOUSE_LATEST_RELEASE headers = {'Accept': GITHUB_API_VERSION} try: response = httpx.get(lighthouse_gh_release_url, headers=headers, follow_redirects=True) except httpx.RequestError as exception: log.error( f'Exception while downloading lighthouse binary. {exception}') return False if response.status_code != 200: log.error(f'HTTP error while downloading lighthouse binary. ' f'Status code {response.status_code}') return False release_json = response.json() if 'assets' not in release_json: log.error('No assets in Github release for lighthouse.') return False binary_asset = None signature_asset = None for asset in release_json['assets']: if 'name' not in asset: continue if 'browser_download_url' not in asset: continue file_name = asset['name'] file_url = asset['browser_download_url'] if file_name.endswith('x86_64-unknown-linux-gnu.tar.gz'): binary_asset = {'file_name': file_name, 'file_url': file_url} elif file_name.endswith('x86_64-unknown-linux-gnu.tar.gz.asc'): signature_asset = {'file_name': file_name, 'file_url': file_url} if binary_asset is None or signature_asset is None: log.error( 'Could not find binary or signature asset in Github release.') return False # Downloading latest Lighthouse release files download_path = Path(Path.home(), 'ethwizard', 'downloads') download_path.mkdir(parents=True, exist_ok=True) binary_path = Path(download_path, binary_asset['file_name']) try: with open(binary_path, 'wb') as binary_file: with httpx.stream('GET', binary_asset['file_url'], follow_redirects=True) as http_stream: if http_stream.status_code != 200: log.error( f'HTTP error while downloading Lighthouse binary from Github. ' f'Status code {http_stream.status_code}') return False for data in http_stream.iter_bytes(): binary_file.write(data) except httpx.RequestError as exception: log.error( f'Exception while downloading Lighthouse binary from Github. {exception}' ) return False signature_path = Path(download_path, signature_asset['file_name']) try: with open(signature_path, 'wb') as signature_file: with httpx.stream('GET', signature_asset['file_url'], follow_redirects=True) as http_stream: if http_stream.status_code != 200: log.error( f'HTTP error while downloading Lighthouse signature from Github. ' f'Status code {http_stream.status_code}') return False for data in http_stream.iter_bytes(): signature_file.write(data) except httpx.RequestError as exception: log.error( f'Exception while downloading Lighthouse signature from Github. {exception}' ) return False # Test if gpg is already installed gpg_is_installed = False try: gpg_is_installed = is_package_installed('gpg') except Exception: return False if not gpg_is_installed: # Install gpg using APT subprocess.run(['apt', '-y', 'update']) subprocess.run(['apt', '-y', 'install', 'gpg']) # Verify PGP signature command_line = [ 'gpg', '--list-keys', '--with-colons', LIGHTHOUSE_PRIME_PGP_KEY_ID ] process_result = subprocess.run(command_line) pgp_key_found = process_result.returncode == 0 if not pgp_key_found: retry_index = 0 retry_count = 15 key_server = PGP_KEY_SERVERS[retry_index % len(PGP_KEY_SERVERS)] log.info(f'Downloading Sigma Prime\'s PGP key from {key_server} ...') command_line = [ 'gpg', '--keyserver', key_server, '--recv-keys', LIGHTHOUSE_PRIME_PGP_KEY_ID ] process_result = subprocess.run(command_line) if process_result.returncode != 0: # GPG failed to download Sigma Prime's PGP key, let's wait and retry a few times while process_result.returncode != 0 and retry_index < retry_count: retry_index = retry_index + 1 delay = 5 log.warning( f'GPG failed to download the PGP key. We will wait {delay} seconds ' f'and try again from a different server.') time.sleep(delay) key_server = PGP_KEY_SERVERS[retry_index % len(PGP_KEY_SERVERS)] log.info( f'Downloading Sigma Prime\'s PGP key from {key_server} ...' ) command_line = [ 'gpg', '--keyserver', key_server, '--recv-keys', LIGHTHOUSE_PRIME_PGP_KEY_ID ] process_result = subprocess.run(command_line) if process_result.returncode != 0: log.error(f''' We failed to download the Sigma Prime's PGP key to verify the lighthouse binary after {retry_count} retries. ''') return False process_result = subprocess.run(['gpg', '--verify', signature_path]) if process_result.returncode != 0: log.error('The lighthouse binary signature is wrong. ' 'We will stop here to protect you.') return False # Stopping Lighthouse services before updating the binary log.info('Stopping Lighthouse services...') subprocess.run([ 'systemctl', 'stop', LIGHTHOUSE_BN_SYSTEMD_SERVICE_NAME, LIGHTHOUSE_VC_SYSTEMD_SERVICE_NAME ]) # Extracting the Lighthouse binary archive log.info('Updating Lighthouse binary...') subprocess.run([ 'tar', 'xvf', binary_path, '--directory', LIGHTHOUSE_INSTALLED_DIRECTORY ]) # Restarting Lighthouse services after updating the binary log.info('Starting Lighthouse services...') subprocess.run([ 'systemctl', 'start', LIGHTHOUSE_BN_SYSTEMD_SERVICE_NAME, LIGHTHOUSE_VC_SYSTEMD_SERVICE_NAME ]) # Remove download leftovers binary_path.unlink() signature_path.unlink() return True
def perform_maintenance(execution_client, execution_client_details, consensus_client, consensus_client_details): # Perform all the maintenance tasks if execution_client == EXECUTION_CLIENT_GETH: # Geth maintenance tasks if execution_client_details[ 'next_step'] == MAINTENANCE_RESTART_SERVICE: log.info('Restarting Geth service...') subprocess.run(['systemctl', 'restart', GETH_SYSTEMD_SERVICE_NAME]) elif execution_client_details[ 'next_step'] == MAINTENANCE_UPGRADE_CLIENT: if not upgrade_geth(): log.error('We could not upgrade the Geth client.') return False elif execution_client_details[ 'next_step'] == MAINTENANCE_START_SERVICE: log.info('Starting Geth service...') subprocess.run(['systemctl', 'start', GETH_SYSTEMD_SERVICE_NAME]) elif execution_client_details[ 'next_step'] == MAINTENANCE_REINSTALL_CLIENT: log.warn('TODO: Reinstalling client is to be implemented.') else: log.error(f'Unknown execution client {execution_client}.') return False if consensus_client == CONSENSUS_CLIENT_LIGHTHOUSE: # Lighthouse maintenance tasks if consensus_client_details[ 'next_step'] == MAINTENANCE_RESTART_SERVICE: log.info('Restarting Lighthouse services...') subprocess.run([ 'systemctl', 'restart', LIGHTHOUSE_BN_SYSTEMD_SERVICE_NAME, LIGHTHOUSE_VC_SYSTEMD_SERVICE_NAME ]) elif consensus_client_details[ 'next_step'] == MAINTENANCE_UPGRADE_CLIENT: if not upgrade_lighthouse(): log.error('We could not upgrade the Lighthouse client.') return False elif consensus_client_details[ 'next_step'] == MAINTENANCE_START_SERVICE: log.info('Starting Lighthouse services...') subprocess.run([ 'systemctl', 'start', LIGHTHOUSE_BN_SYSTEMD_SERVICE_NAME, LIGHTHOUSE_VC_SYSTEMD_SERVICE_NAME ]) elif consensus_client_details[ 'next_step'] == MAINTENANCE_REINSTALL_CLIENT: log.warn('TODO: Reinstalling client is to be implemented.') else: log.error(f'Unknown consensus client {consensus_client}.') return False return True
def show_dashboard(context): # Show simple dashboard selected_execution_client = CTX_SELECTED_EXECUTION_CLIENT selected_consensus_client = CTX_SELECTED_CONSENSUS_CLIENT current_execution_client = context[selected_execution_client] current_consensus_client = context[selected_consensus_client] # Get execution client details execution_client_details = get_execution_client_details( current_execution_client) if not execution_client_details: log.error('Unable to get execution client details.') return False # Find out if we need to do maintenance for the execution client execution_client_details['next_step'] = MAINTENANCE_DO_NOTHING installed_version = execution_client_details['versions']['installed'] if installed_version != UNKNOWN_VALUE: installed_version = parse_version(installed_version) running_version = execution_client_details['versions']['running'] if running_version != UNKNOWN_VALUE: running_version = parse_version(running_version) available_version = execution_client_details['versions']['available'] if available_version != UNKNOWN_VALUE: available_version = parse_version(available_version) latest_version = execution_client_details['versions']['latest'] if latest_version != UNKNOWN_VALUE: latest_version = parse_version(latest_version) # If the available version is older than the latest one, we need to check again soon # It simply means that the updated build is not available yet for installing if is_version(latest_version) and is_version(available_version): if available_version < latest_version: execution_client_details[ 'next_step'] = MAINTENANCE_CHECK_AGAIN_SOON # If the service is not running, we need to start it if not execution_client_details['service']['running']: execution_client_details['next_step'] = MAINTENANCE_START_SERVICE # If the running version is older than the installed one, we need to restart the service if is_version(installed_version) and is_version(running_version): if running_version < installed_version: execution_client_details['next_step'] = MAINTENANCE_RESTART_SERVICE # If the installed version is older than the available one, we need to upgrade the client if is_version(installed_version) and is_version(available_version): if installed_version < available_version: execution_client_details['next_step'] = MAINTENANCE_UPGRADE_CLIENT # If the service is not installed or found, we need to reinstall the client if not execution_client_details['service']['found']: execution_client_details['next_step'] = MAINTENANCE_REINSTALL_CLIENT # Get consensus client details consensus_client_details = get_consensus_client_details( current_consensus_client) if not consensus_client_details: log.error('Unable to get consensus client details.') return False # Find out if we need to do maintenance for the consensus client consensus_client_details['next_step'] = MAINTENANCE_DO_NOTHING installed_version = consensus_client_details['versions']['installed'] if installed_version != UNKNOWN_VALUE: installed_version = parse_version(installed_version) running_version = consensus_client_details['versions']['running'] if running_version != UNKNOWN_VALUE: running_version = parse_version(running_version) latest_version = consensus_client_details['versions']['latest'] if latest_version != UNKNOWN_VALUE: latest_version = parse_version(latest_version) # If the service is not running, we need to start it if not consensus_client_details['bn_service']['running']: consensus_client_details['next_step'] = MAINTENANCE_START_SERVICE if not consensus_client_details['vc_service']['running']: consensus_client_details['next_step'] = MAINTENANCE_START_SERVICE # If the running version is older than the installed one, we need to restart the services if is_version(installed_version) and is_version(running_version): if running_version < installed_version: consensus_client_details['next_step'] = MAINTENANCE_RESTART_SERVICE # If the installed version is older than the latest one, we need to upgrade the client if is_version(installed_version) and is_version(latest_version): if installed_version < latest_version: consensus_client_details['next_step'] = MAINTENANCE_UPGRADE_CLIENT # If the service is not installed or found, we need to reinstall the client if (not consensus_client_details['bn_service']['found'] or not consensus_client_details['vc_service']['found']): consensus_client_details['next_step'] = MAINTENANCE_REINSTALL_CLIENT # We only need to do maintenance if either the execution or the consensus client needs # maintenance. maintenance_needed = ( execution_client_details['next_step'] != MAINTENANCE_DO_NOTHING or consensus_client_details['next_step'] != MAINTENANCE_DO_NOTHING) # Build the dashboard with the details we have maintenance_tasks_description = { MAINTENANCE_DO_NOTHING: 'Nothing to perform here. Everything is good.', MAINTENANCE_RESTART_SERVICE: 'Service needs to be restarted.', MAINTENANCE_UPGRADE_CLIENT: 'Client needs to be upgraded.', MAINTENANCE_CHECK_AGAIN_SOON: 'Check again. Client update should be available soon.', MAINTENANCE_START_SERVICE: 'Service needs to be started.', MAINTENANCE_REINSTALL_CLIENT: 'Client needs to be reinstalled.', } buttons = [ ('Quit', False), ] maintenance_message = 'Nothing is needed in terms of maintenance.' if maintenance_needed: buttons = [ ('Maintain', 1), ('Quit', False), ] maintenance_message = 'Some maintenance tasks are pending. Select maintain to perform them.' ec_section = ( f'<b>Geth</b> details (I: {execution_client_details["versions"]["installed"]}, ' f'R: {execution_client_details["versions"]["running"]}, ' f'A: {execution_client_details["versions"]["available"]}, ' f'L: {execution_client_details["versions"]["latest"]})\n' f'Service is running: {execution_client_details["service"]["running"]}\n' f'<b>Maintenance task</b>: {maintenance_tasks_description.get(execution_client_details["next_step"], UNKNOWN_VALUE)}' ) cc_section = ( f'<b>Lighthouse</b> details (I: {consensus_client_details["versions"]["installed"]}, ' f'R: {consensus_client_details["versions"]["running"]}, ' f'L: {consensus_client_details["versions"]["latest"]})\n' f'Running services - Beacon node: {consensus_client_details["bn_service"]["running"]}, Validator client: {consensus_client_details["vc_service"]["running"]}\n' f'<b>Maintenance task</b>: {maintenance_tasks_description.get(consensus_client_details["next_step"], UNKNOWN_VALUE)}' ) result = button_dialog(title='Maintenance Dashboard', text=(HTML(f''' Here are some details about your Ethereum clients. {ec_section} {cc_section} {maintenance_message} Versions legend - I: Installed, R: Running, A: Available, L: Latest ''')), buttons=buttons).run() if not result: return False if result == 1: if perform_maintenance(current_execution_client, execution_client_details, current_consensus_client, consensus_client_details): return show_dashboard(context) else: log.error('We could not perform all the maintenance tasks.') return False
def get_consensus_client_details(consensus_client): # Get the details for the current consensus client if consensus_client == CONSENSUS_CLIENT_LIGHTHOUSE: details = { 'bn_service': { 'found': False, 'load': UNKNOWN_VALUE, 'active': UNKNOWN_VALUE, 'sub': UNKNOWN_VALUE }, 'vc_service': { 'found': False, 'load': UNKNOWN_VALUE, 'active': UNKNOWN_VALUE, 'sub': UNKNOWN_VALUE }, 'versions': { 'installed': UNKNOWN_VALUE, 'running': UNKNOWN_VALUE, 'latest': UNKNOWN_VALUE } } # Check for existing systemd services lighthouse_bn_service_exists = False lighthouse_bn_service_name = LIGHTHOUSE_BN_SYSTEMD_SERVICE_NAME service_details = get_systemd_service_details( lighthouse_bn_service_name) if service_details['LoadState'] == 'loaded': lighthouse_bn_service_exists = True details['bn_service']['found'] = True details['bn_service']['load'] = service_details['LoadState'] details['bn_service']['active'] = service_details['ActiveState'] details['bn_service']['sub'] = service_details['SubState'] details['bn_service']['running'] = is_service_running( service_details) lighthouse_vc_service_exists = False lighthouse_vc_service_name = LIGHTHOUSE_VC_SYSTEMD_SERVICE_NAME service_details = get_systemd_service_details( lighthouse_vc_service_name) if service_details['LoadState'] == 'loaded': lighthouse_vc_service_exists = True details['vc_service']['found'] = True details['vc_service']['load'] = service_details['LoadState'] details['vc_service']['active'] = service_details['ActiveState'] details['vc_service']['sub'] = service_details['SubState'] details['vc_service']['running'] = is_service_running( service_details) details['versions']['installed'] = get_lighthouse_installed_version() details['versions']['running'] = get_lighthouse_running_version() details['versions']['latest'] = get_lighthouse_latest_version() return details else: log.error(f'Unknown consensus client {consensus_client}.') return False