From 11f9a5d035818553da2a9fdc76fb644adca68b66 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 11 Nov 2018 09:16:11 -0800 Subject: [PATCH] If at first you don't succeed (to connect to the Teacher node), try try again (and catch the appropriate errors). --- nucypher/cli.py | 105 ++++++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/nucypher/cli.py b/nucypher/cli.py index 5d3880983..dd553b40c 100644 --- a/nucypher/cli.py +++ b/nucypher/cli.py @@ -18,27 +18,27 @@ You should have received a copy of the GNU General Public License along with nucypher. If not, see . """ - - -from ipaddress import ip_address - import collections import hashlib import json import os import shutil +import socket +from ipaddress import ip_address from typing import Tuple, ClassVar from urllib.parse import urlparse import click +import requests +import time from constant_sorrow.constants import NO_NODE_CONFIGURATION, NO_BLOCKCHAIN_CONNECTION from eth_utils import is_checksum_address +from nacl.exceptions import CryptoError from sentry_sdk.integrations.logging import LoggingIntegration from twisted.internet import stdio from twisted.logger import Logger from twisted.logger import globalLogPublisher from web3.middleware import geth_poa_middleware -from nacl.exceptions import CryptoError import nucypher from nucypher.blockchain.eth.agents import MinerAgent, PolicyAgent, NucypherTokenAgent, EthereumContractAgent @@ -73,7 +73,6 @@ BANNER = """ class NucypherClickConfig: - __LOG_TO_SENTRY_ENVVAR = "NUCYPHER_SENTRY_LOGS" __NUCYPHER_SENTRY_ENDPOINT = "https://d8af7c4d692e4692a455328a280d845e@sentry.io/1310685" _KEYRING_PASSPHRASE_ENVVAR = "NUCYPHER_KEYRING_PASSPHRASE" @@ -90,7 +89,7 @@ class NucypherClickConfig: import sentry_sdk import logging sentry_logging = LoggingIntegration( - level=logging.INFO, # Capture info and above as breadcrumbs + level=logging.INFO, # Capture info and above as breadcrumbs event_level=logging.DEBUG # Send debug logs as events ) sentry_sdk.init( @@ -134,7 +133,8 @@ class NucypherClickConfig: node_configuration = configuration_class.from_configuration_file(filepath=filepath) except FileNotFoundError: if self.config_root: - node_configuration = configuration_class(temp=False, config_root=self.config_root, auto_initialize=False) + node_configuration = configuration_class(temp=False, config_root=self.config_root, + auto_initialize=False) else: node_configuration = configuration_class(federated_only=self.federated_only, auto_initialize=False, @@ -169,7 +169,8 @@ class NucypherClickConfig: def create_account(self, passphrase: str = None) -> str: """Creates a new local or hosted ethereum wallet""" - choice = click.prompt("Create a new Hosted or Local account?", default='hosted', type=click.STRING).strip().lower() + choice = click.prompt("Create a new Hosted or Local account?", default='hosted', + type=click.STRING).strip().lower() if choice not in ('hosted', 'local'): click.echo("Invalid Input") raise click.Abort() @@ -190,7 +191,8 @@ class NucypherClickConfig: raise click.BadParameter("Invalid choice; Options are hosted or local.") return new_address - def _collect_pending_configuration_details(self, ursula: bool=False, force: bool = False, rest_host=None) -> PendingConfigurationDetails: + def _collect_pending_configuration_details(self, ursula: bool = False, force: bool = False, + rest_host=None) -> PendingConfigurationDetails: # Defaults passphrase = None @@ -225,11 +227,12 @@ class NucypherClickConfig: details = self.PendingConfigurationDetails(passphrase=passphrase, wallet=generate_wallet, signing=generate_encrypting_keys, tls=generate_tls_keys, - skip_keys=skip_all_key_generation, save_file=save_node_configuration_file) + skip_keys=skip_all_key_generation, + save_file=save_node_configuration_file) return details def create_new_configuration(self, - ursula: bool=False, + ursula: bool = False, force: bool = False, rest_host: str = None, no_registry: bool = False): @@ -251,7 +254,8 @@ class NucypherClickConfig: self.node_configuration.federated_only = self.federated_only try: - pending_config = self._collect_pending_configuration_details(force=force, ursula=ursula, rest_host=rest_host) + pending_config = self._collect_pending_configuration_details(force=force, ursula=ursula, + rest_host=rest_host) new_installation_path = self.node_configuration.initialize(passphrase=pending_config.passphrase, wallet=pending_config.wallet, encrypting=pending_config.signing, @@ -348,6 +352,7 @@ class IPv4Address(click.ParamType): IPV4_ADDRESS = IPv4Address() CHECKSUM_ADDRESS = ChecksumAddress() + ######################################## @@ -356,16 +361,20 @@ CHECKSUM_ADDRESS = ChecksumAddress() # @click.group() -@click.option('--version', help="Echo the CLI version", is_flag=True, callback=echo_version, expose_value=False, is_eager=True) +@click.option('--version', help="Echo the CLI version", is_flag=True, callback=echo_version, expose_value=False, + is_eager=True) @click.option('-v', '--verbose', help="Specify verbosity level", count=True) @click.option('--dev', help="Run in development mode", is_flag=True) @click.option('--federated-only', help="Connect only to federated nodes", is_flag=True, default=True) @click.option('--config-root', help="Custom configuration directory", type=click.Path()) -@click.option('--config-file', help="Path to configuration file", type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)) -@click.option('--metadata-dir', help="Custom known metadata directory", type=click.Path(exists=True, dir_okay=True, file_okay=False, writable=True)) +@click.option('--config-file', help="Path to configuration file", + type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)) +@click.option('--metadata-dir', help="Custom known metadata directory", + type=click.Path(exists=True, dir_okay=True, file_okay=False, writable=True)) @click.option('--provider-uri', help="Blockchain provider's URI", type=click.STRING) @click.option('--compile/--no-compile', help="Compile solidity from source files", is_flag=True) -@click.option('--registry-filepath', help="Custom contract registry filepath", type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)) +@click.option('--registry-filepath', help="Custom contract registry filepath", + type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)) @click.option('--deployer', help="Connect using a deployer's blockchain interface", is_flag=True) @click.option('--poa', help="Inject POA middleware", is_flag=True) @uses_config @@ -381,7 +390,6 @@ def cli(config, deployer, compile, poa): - click.echo(BANNER) # Store config data @@ -422,7 +430,7 @@ def configure(config, """Manage Ursula node system configuration""" # Fetch Existing Configuration - config.get_node_configuration(configuration_class=UrsulaConfiguration, rest_host=rest_host) + config.get_node_configuration(configuration_class=UrsulaConfiguration, rest_host=rest_host) if action == "destroy": config.destroy_configuration() @@ -486,16 +494,20 @@ def accounts(config, click.secho("Created new ETH address {}".format(new_address), fg='blue') if click.confirm("Set new address as the node's keying default account?".format(new_address)): config.blockchain.interface.w3.eth.defaultAccount = new_address - click.echo("{} is now the node's default account.".format(config.blockchain.interface.w3.eth.defaultAccount)) + click.echo( + "{} is now the node's default account.".format(config.blockchain.interface.w3.eth.defaultAccount)) if action == 'set-default': - config.blockchain.interface.w3.eth.defaultAccount = checksum_address # TODO: is there a better way to do this? + config.blockchain.interface.w3.eth.defaultAccount = checksum_address # TODO: is there a better way to do this? click.echo("{} is now the node's default account.".format(config.blockchain.interface.w3.eth.defaultAccount)) elif action == 'export': keyring = NucypherKeyring(account=checksum_address) - click.confirm("Export local private key for {} to node's keyring: {}?".format(checksum_address, config.provider_uri), abort=True) - passphrase = click.prompt("Enter passphrase to decrypt account", type=click.STRING, hide_input=True, confirmation_prompt=True) + click.confirm( + "Export local private key for {} to node's keyring: {}?".format(checksum_address, config.provider_uri), + abort=True) + passphrase = click.prompt("Enter passphrase to decrypt account", type=click.STRING, hide_input=True, + confirmation_prompt=True) keyring._export_wallet_to_node(blockchain=config.blockchain, passphrase=passphrase) elif action == 'list': @@ -503,7 +515,8 @@ def accounts(config, token_balance = config.token_agent.get_balance(address=checksum_address) eth_balance = config.blockchain.interface.w3.eth.getBalance(checksum_address) base_row_template = ' {address}\n Tokens: {tokens}\n ETH: {eth}\n ' - row_template = ('\netherbase |'+base_row_template) if not index else '{index} ....... |'+base_row_template + row_template = ( + '\netherbase |' + base_row_template) if not index else '{index} ....... |' + base_row_template row = row_template.format(index=index, address=checksum_address, tokens=token_balance, eth=eth_balance) click.secho(row, fg='blue') @@ -513,7 +526,8 @@ def accounts(config, click.echo('No checksum_address supplied, Using the default {}'.format(checksum_address)) token_balance = config.token_agent.get_balance(address=checksum_address) eth_balance = config.token_agent.blockchain.interface.w3.eth.getBalance(checksum_address) - click.secho("Balance of {} | Tokens: {} | ETH: {}".format(checksum_address, token_balance, eth_balance), fg='blue') + click.secho("Balance of {} | Tokens: {} | ETH: {}".format(checksum_address, token_balance, eth_balance), + fg='blue') elif action == "transfer-tokens": destination, amount = __collect_transfer_details(denomination='tokens') @@ -536,8 +550,10 @@ def accounts(config, @cli.command() @click.option('--checksum-address', type=CHECKSUM_ADDRESS) -@click.option('--value', help="Token value of stake", type=click.IntRange(min=MIN_ALLOWED_LOCKED, max=MIN_ALLOWED_LOCKED, clamp=False)) -@click.option('--duration', help="Period duration of stake", type=click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False)) +@click.option('--value', help="Token value of stake", + type=click.IntRange(min=MIN_ALLOWED_LOCKED, max=MIN_ALLOWED_LOCKED, clamp=False)) +@click.option('--duration', help="Period duration of stake", + type=click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False)) @click.option('--index', help="A specific stake index to resume", type=click.INT) @click.argument('action', default='list', required=False) @uses_config @@ -672,7 +688,7 @@ def stake(config, """.format(stakes[index], target_value, - target_value+extension)) + target_value + extension)) click.confirm("Is this correct?", abort=True) config.miner_agent.divide_stake(miner_address=checksum_address, @@ -699,7 +715,8 @@ def stake(config, @click.option('--contract-name', help="Deploy a single contract by name", type=click.STRING) @click.option('--force', is_flag=True) @click.option('--deployer-address', help="Deployer's checksum address", type=CHECKSUM_ADDRESS) -@click.option('--registry-outfile', help="Output path for new registry", type=click.Path(), default=NodeConfiguration.REGISTRY_SOURCE) +@click.option('--registry-outfile', help="Output path for new registry", type=click.Path(), + default=NodeConfiguration.REGISTRY_SOURCE) @click.argument('action') @uses_config def deploy(config, @@ -814,7 +831,9 @@ def deploy(config, try: deployer_info = deployers[contract_name] except KeyError: - click.secho("No such contract {}. Available contracts are {}".format(contract_name, available_deployers), fg='red', bold=True) + click.secho( + "No such contract {}. Available contracts are {}".format(contract_name, available_deployers), + fg='red', bold=True) raise click.Abort() else: _txs, _agent = __deploy_contract(deployer_info.deployer_class, @@ -838,7 +857,7 @@ def deploy(config, click.echo("{}:{}".format(tx_name, txhash)) if not force and click.confirm("Save transaction hashes to JSON file?"): - file = click.prompt("Enter output filepath", type=click.File(mode='w')) # TODO: Save Txhashes + file = click.prompt("Enter output filepath", type=click.File(mode='w')) # TODO: Save Txhashes file.write(json.dumps(__deployment_transactions)) click.secho("Successfully wrote transaction hashes file to {}".format(file.path), fg='green') @@ -908,7 +927,7 @@ def status(config): # Heading label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes) - heading = '\n'+label+" "*(45-len(label))+"Last Seen " + heading = '\n' + label + " " * (45 - len(label)) + "Last Seen " click.secho(heading, bold=True, nl=False) # Legend @@ -968,6 +987,7 @@ def ursula(config, 4. Run TLS deployment (Learning Loop + Reactor) """ + log = Logger("ursula/launch") password = os.environ.get(config._KEYRING_PASSPHRASE_ENVVAR, None) if not password: @@ -978,7 +998,7 @@ def ursula(config, raise click.BadArgumentUsage("No Configuration file found, and no --checksum address was provided.") if not checksum_address and not config.dev: raise click.BadOptionUsage(message="No account specified. pass --checksum-address, --dev, " - "or use a configuration file with --config-file ") + "or use a configuration file with --config-file ") return UrsulaConfiguration(temp=config.dev, auto_initialize=config.dev, @@ -1033,13 +1053,18 @@ def ursula(config, raise click.BadParameter("Invalid teacher URI. Is the hostname prefixed with 'https://' ?") port = parsed_teacher_uri.port or UrsulaConfiguration.DEFAULT_REST_PORT - teacher = Ursula.from_seed_and_stake_info(host=parsed_teacher_uri.hostname, - port=port, - federated_only=ursula_config.federated_only, - checksum_address=checksum_address, - minimum_stake=min_stake, - certificates_directory=ursula_config.known_certificates_dir) - teacher_nodes.append(teacher) + while not teacher_nodes: + try: + teacher = Ursula.from_seed_and_stake_info(host=parsed_teacher_uri.hostname, + port=port, + federated_only=ursula_config.federated_only, + checksum_address=checksum_address, + minimum_stake=min_stake, + certificates_directory=ursula_config.known_certificates_dir) + teacher_nodes.append(teacher) + except (socket.gaierror, requests.exceptions.ConnectionError, ConnectionRefusedError): + log.warn("Can't connect to seed node. Will retry.") + time.sleep(5) # # Produce