diff --git a/cli/main.py b/cli/main.py index 08e94f0b2..0fba9c5a1 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 + """ This file is part of nucypher. @@ -14,47 +15,43 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with nucypher. If not, see . + """ import collections import hashlib import json -import logging import os import shutil -import sys from typing import Tuple, ClassVar import click -from constant_sorrow import constants +from constant_sorrow.constants import NO_NODE_CONFIGURATION, NO_BLOCKCHAIN_CONNECTION from eth_utils import is_checksum_address +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 import nucypher from nucypher.blockchain.eth.agents import MinerAgent, PolicyAgent, NucypherTokenAgent, EthereumContractAgent from nucypher.blockchain.eth.chains import Blockchain - from nucypher.blockchain.eth.constants import (MIN_ALLOWED_LOCKED, MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS) - from nucypher.blockchain.eth.deployers import (NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer) - from nucypher.characters.lawful import Ursula from nucypher.config.characters import UrsulaConfiguration - from nucypher.config.constants import (SEEDNODES, SeednodeMetadata, NUCYPHER_SENTRY_ENDPOINT, - REPORT_TO_SENTRY, - DEBUG, KEYRING_PASSPHRASE_ENVVAR_KEY) - from nucypher.config.keyring import NucypherKeyring from nucypher.config.node import NodeConfiguration +from nucypher.utilities.logging import logToSentry from nucypher.utilities.sandbox.ursula import UrsulaCommandProtocol BANNER = """ @@ -71,68 +68,53 @@ BANNER = """ """.format(nucypher.__version__) - -def echo_version(ctx, param, value): - if not value or ctx.resilient_parsing: - return - click.secho(BANNER, bold=True) - ctx.exit() - - -# Setup Logging # -################ - -root = logging.Logger("cli") -if DEBUG: - root.setLevel(logging.DEBUG) - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - ch.setFormatter(formatter) - root.addHandler(ch) - -# Report to Sentry # -#################### - -if not hasattr(sys, '_pytest_is_running') and REPORT_TO_SENTRY: - import sentry_sdk - sentry_sdk.init(NUCYPHER_SENTRY_ENDPOINT, release=nucypher.__version__) - -#################### - # Pending Configuration Named Tuple -fields = 'passphrase wallet signing tls skip_keys save_file'.split() -PendingConfigurationDetails = collections.namedtuple('PendingConfigurationDetails', fields) +PendingConfigurationDetails = collections.namedtuple('PendingConfigurationDetails', + 'passphrase wallet signing tls skip_keys save_file') class NucypherClickConfig: + log_to_sentry = True + def __init__(self): - self.log = logging.Logger(self.__class__.__name__) + if self.log_to_sentry: + import sentry_sdk + import logging + sentry_logging = LoggingIntegration( + level=logging.INFO, # Capture info and above as breadcrumbs + event_level=logging.DEBUG # Send debug logs as events + ) + sentry_sdk.init( + dsn=NUCYPHER_SENTRY_ENDPOINT, + integrations=[sentry_logging], + release=nucypher.__version__ + ) + + globalLogPublisher.addObserver(logToSentry) + + self.log = Logger(self.__class__.__name__) # Node Configuration - self.node_configuration = constants.NO_NODE_CONFIGURATION - self.dev = constants.NO_NODE_CONFIGURATION - self.federated_only = constants.NO_NODE_CONFIGURATION - self.config_root = constants.NO_NODE_CONFIGURATION - self.config_file = constants.NO_NODE_CONFIGURATION + self.node_configuration = NO_NODE_CONFIGURATION + self.dev = NO_NODE_CONFIGURATION + self.federated_only = NO_NODE_CONFIGURATION + self.config_root = NO_NODE_CONFIGURATION + self.config_file = NO_NODE_CONFIGURATION # Blockchain - self.deployer = constants.NO_BLOCKCHAIN_CONNECTION - self.compile = constants.NO_BLOCKCHAIN_CONNECTION - self.poa = constants.NO_BLOCKCHAIN_CONNECTION - self.blockchain = constants.NO_BLOCKCHAIN_CONNECTION - self.provider_uri = constants.NO_BLOCKCHAIN_CONNECTION - self.registry_filepath = constants.NO_BLOCKCHAIN_CONNECTION - self.accounts = constants.NO_BLOCKCHAIN_CONNECTION + self.deployer = NO_BLOCKCHAIN_CONNECTION + self.compile = NO_BLOCKCHAIN_CONNECTION + self.poa = NO_BLOCKCHAIN_CONNECTION + self.blockchain = NO_BLOCKCHAIN_CONNECTION + self.provider_uri = NO_BLOCKCHAIN_CONNECTION + self.registry_filepath = NO_BLOCKCHAIN_CONNECTION + self.accounts = NO_BLOCKCHAIN_CONNECTION # Agency - self.token_agent = constants.NO_BLOCKCHAIN_CONNECTION - self.miner_agent = constants.NO_BLOCKCHAIN_CONNECTION - self.policy_agent = constants.NO_BLOCKCHAIN_CONNECTION - - # Simulation - self.sim_processes = constants.NO_SIMULATION_RUNNING + self.token_agent = NO_BLOCKCHAIN_CONNECTION + self.miner_agent = NO_BLOCKCHAIN_CONNECTION + self.policy_agent = NO_BLOCKCHAIN_CONNECTION def get_node_configuration(self, configuration_class=UrsulaConfiguration, **overrides): if self.dev: @@ -204,7 +186,6 @@ class NucypherClickConfig: # Defaults passphrase = None - host = UrsulaConfiguration.DEFAULT_REST_HOST skip_all_key_generation, generate_wallet = False, False generate_encrypting_keys, generate_tls_keys, save_node_configuration_file = True, True, True @@ -218,7 +199,7 @@ class NucypherClickConfig: if generate_tls_keys or force: if not force and not rest_host: - rest_host = click.prompt("Enter node IPv4 Address", + rest_host = click.prompt("Enter Node's Public IPv4 Address", default=UrsulaConfiguration.DEFAULT_REST_HOST, type=click.STRING) @@ -299,10 +280,19 @@ Located at {}?'''.format(self.node_configuration.config_root), abort=True) click.secho("Deleted configuration files at {}".format(self.node_configuration.config_root), fg='blue') +######################################### + # Register the above class as a decorator uses_config = click.make_pass_decorator(NucypherClickConfig, ensure=True) +def echo_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + click.secho(BANNER, bold=True) + ctx.exit() + + # Custom input type class ChecksumAddress(click.ParamType): name = 'checksum_address' @@ -315,6 +305,12 @@ class ChecksumAddress(click.ParamType): CHECKSUM_ADDRESS = ChecksumAddress() +######################################## + + +# +# CLI Commands +# @click.group() @click.option('--version', help="Echo the CLI version", is_flag=True, callback=echo_version, expose_value=False, is_eager=True) @@ -348,6 +344,7 @@ def cli(config, # Store config data config.verbose = verbose + # CLI Node View config.dev = dev config.federated_only = federated_only config.config_root = config_root @@ -359,14 +356,6 @@ def cli(config, config.deployer = deployer config.poa = poa - # TODO: Create NodeConfiguration from these values - # node_configuration = NodeConfiguration(temp=dev, - # federated_only=federated_only, - # config_root=config_root, - # known_metadata_dir=metadata_dir, - # registry_filepath=registry_filepath, - # ) - if config.verbose: click.secho("Verbose mode is enabled", fg='blue') @@ -378,7 +367,6 @@ def cli(config, @cli.command() @click.option('--ursula', help="Configure ursula", is_flag=True, default=False) -@click.option('--filesystem', is_flag=True, default=False) @click.option('--rest-host', type=click.STRING) @click.option('--no-registry', help="Skip importing the default contract registry", is_flag=True) @click.option('--force', help="Ask confirm once; Do not generate wallet or certificate", is_flag=True) @@ -388,32 +376,22 @@ def configure(config, action, ursula, rest_host, - filesystem, no_registry, force): - """Manage local nucypher files and directories""" - config.get_node_configuration(configuration_class=UrsulaConfiguration, rest_host=rest_host) # TODO: Un-hardcode Ursula + + # Fetch Existing Configuration + config.get_node_configuration(configuration_class=UrsulaConfiguration, rest_host=rest_host) + if action == "install": config.create_new_configuration(ursula=ursula, force=force, no_registry=no_registry, rest_host=rest_host) elif action == "view": - json_config = NodeConfiguration._read_configuration_file(filepath=config.node_configuration.config_file_location) + json_config = UrsulaConfiguration._read_configuration_file(filepath=config.node_configuration.config_file_location) click.echo(json_config) elif action == "reset": config.destroy_configuration() config.create_new_configuration(ursula=ursula, force=force, no_registry=no_registry, rest_host=rest_host) elif action == "destroy": config.destroy_configuration() - elif action == "validate": - is_valid = True # Until there is a reason to believe otherwise... - try: - if filesystem: # Check runtime directory - is_valid = NodeConfiguration.validate(config_root=config.node_configuration.config_root, - no_registry=no_registry) - except NodeConfiguration.InvalidConfiguration: - is_valid = False - finally: - result = 'Valid' if is_valid else 'Invalid' - click.echo('{} is {}'.format(config.node_configuration.config_root, result)) else: raise click.BadArgumentUsage("No such argument {}".format(action)) @@ -439,13 +417,13 @@ def accounts(config, checksum_address = config.blockchain.interface.w3.eth.coinbase click.echo("WARNING: No checksum address specified - Using the node's default account.") - def __collect_transfer_details(denomination: str): - destination = click.prompt("Enter destination checksum_address") - if not is_checksum_address(destination): - click.secho("{} is not a valid checksum checksum_address".format(destination), fg='red', bold=True) - raise click.Abort() - amount = click.prompt("Enter amount of {} to transfer".format(denomination), type=click.INT) - return destination, amount + def __collect_transfer_details(denomination: str): + destination = click.prompt("Enter destination checksum_address") + if not is_checksum_address(destination): + click.secho("{} is not a valid checksum checksum_address".format(destination), fg='red', bold=True) + raise click.Abort() + amount = click.prompt("Enter amount of {} to transfer".format(denomination), type=click.INT) + return destination, amount # # Action Switch @@ -565,7 +543,7 @@ def stake(config, address = config.accounts[account_selection] if action == 'list': - live_stakes = config.miner_agent.get_all_stakes(miner_address=address) + live_stakes = config.miner_agent.get_all_stakes(miner_address=checksum_address) for index, stake_info in enumerate(live_stakes): row = '{} | {}'.format(index, stake_info) click.echo(row) @@ -573,12 +551,12 @@ def stake(config, elif action == 'init': click.confirm("Stage a new stake?", abort=True) - live_stakes = config.miner_agent.get_all_stakes(miner_address=address) + live_stakes = config.miner_agent.get_all_stakes(miner_address=checksum_address) if len(live_stakes) > 0: - raise RuntimeError("There is an existing stake for {}".format(address)) + raise RuntimeError("There is an existing stake for {}".format(checksum_address)) # Value - balance = config.token_agent.get_balance(address=address) + balance = config.token_agent.get_balance(address=checksum_address) click.echo("Current balance: {}".format(balance)) value = click.prompt("Enter stake value", type=click.INT) @@ -586,7 +564,7 @@ def stake(config, message = "Minimum duration: {} | Maximum Duration: {}".format(MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS) click.echo(message) - duration = click.prompt("Enter stake duration in days", type=click.INT) + duration = click.prompt("Enter stake duration in periods (1 Period = 24 Hours)", type=click.INT) start_period = config.miner_agent.get_current_period() end_period = start_period + duration @@ -602,45 +580,28 @@ def stake(config, Start Period: {start_period} End Period: {end_period} - """.format(address=address, + """.format(address=checksum_address, value=value, duration=duration, start_period=start_period, end_period=end_period)) # TODO: Ursula Process management - # if not click.confirm("Is this correct?"): - # # field = click.prompt("Which stake field do you want to edit?") - # raise NotImplementedError - # - # # Initialize the staged stake - # config.__proxy_contract.deposit_tokens(amount=value, lock_periods=duration, sender_address=address) - # - # proc_params = ['run_ursula'] - # processProtocol = UrsulaCommandProtocol(command=proc_params, checksum_address=checksum_address) - # ursula_proc = reactor.spawnProcess(processProtocol, "nucypher", proc_params) - raise NotImplementedError - - elif action == 'resume': - """Reconnect and resume an existing live stake""" - # proc_params = ['run_ursula'] - # processProtocol = UrsulaCommandProtocol(command=proc_params, checksum_address=checksum_address) - # ursula_proc = reactor.spawnProcess(processProtocol, "nucypher", proc_params) raise NotImplementedError elif action == 'confirm-activity': """Manually confirm activity for the active period""" - stakes = config.miner_agent.get_all_stakes(miner_address=address) + stakes = config.miner_agent.get_all_stakes(miner_address=checksum_address) if len(stakes) == 0: - raise RuntimeError("There are no active stakes for {}".format(address)) - config.miner_agent.confirm_activity(node_address=address) + raise RuntimeError("There are no active stakes for {}".format(checksum_address)) + config.miner_agent.confirm_activity(node_address=checksum_address) elif action == 'divide': """Divide an existing stake by specifying the new target value and end period""" - stakes = config.miner_agent.get_all_stakes(miner_address=address) + stakes = config.miner_agent.get_all_stakes(miner_address=checksum_address) if len(stakes) == 0: - raise RuntimeError("There are no active stakes for {}".format(address)) + raise RuntimeError("There are no active stakes for {}".format(checksum_address)) if not index: for selection_index, stake_info in enumerate(stakes): @@ -661,7 +622,7 @@ def stake(config, target_value+extension)) click.confirm("Is this correct?", abort=True) - config.miner_agent.divide_stake(miner_address=address, + config.miner_agent.divide_stake(miner_address=checksum_address, stake_index=index, value=value, periods=extension) @@ -723,12 +684,7 @@ def deploy(config, upgradeable=True, agent_name='policy_agent', dependant='miner_agent' - ), - - # UserEscrowDeployer._contract_name: DeployerInfo(deployer_class=UserEscrowDeployer, - # upgradeable=True, - # agent_name='user_agent', - # dependant='policy_agent'), # TODO: User Escrow CLI Deployment + ) }) click.confirm("Continue?", abort=True) @@ -829,7 +785,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 + 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') @@ -838,12 +794,8 @@ def deploy(config, @cli.command() -@click.option('--contracts', help="Echo nucypher smart contract info", is_flag=True) -@click.option('--network', help="Echo the network status", is_flag=True) @uses_config -def status(config, - contracts, - network): +def status(config): """ Echo a snapshot of live network metadata. """ diff --git a/examples/finnegans_wake_demo/finnegans-wake-federated.py b/examples/finnegans_wake_demo/finnegans-wake-federated.py index 60233ddb6..ff61a4ec5 100644 --- a/examples/finnegans_wake_demo/finnegans-wake-federated.py +++ b/examples/finnegans_wake_demo/finnegans-wake-federated.py @@ -44,17 +44,6 @@ from nucypher.network.middleware import RestMiddleware def simpleObserver(event): print(event) - -# Setup logging -# root = Logger() -# root.setLevel(LogLevel.debug) -# -# ch = logging.StreamHandler(sys.stdout) -# ch.setLevel(logging.INFO) -# formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -# ch.setFormatter(formatter) -# root.addHandler(ch) - globalLogPublisher.addObserver(simpleObserver) # Temporary storage area for demo diff --git a/nucypher/__init__.py b/nucypher/__init__.py index 4c571a3d2..f32ffae43 100644 --- a/nucypher/__init__.py +++ b/nucypher/__init__.py @@ -14,9 +14,6 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with nucypher. If not, see . """ - -import sys - from nucypher.__about__ import ( __author__, __license__, __summary__, __title__, __version__, __copyright__, __email__, __url__ ) @@ -32,36 +29,3 @@ __all__ = [ from umbral.config import set_default_curve set_default_curve() - - -# Report to Sentry # -#################### - -from nucypher.config.constants import REPORT_TO_SENTRY, NUCYPHER_SENTRY_ENDPOINT, PACKAGE_NAME - -if not hasattr(sys, '_pytest_is_running'): - try: - import sentry_sdk - from sentry_sdk.integrations.logging import LoggingIntegration - except ImportError: - if REPORT_TO_SENTRY is True: - raise ImportError("Sentry") - else: - import logging - sentry_logging = LoggingIntegration( - level=logging.INFO, # Capture info and above as breadcrumbs - event_level=logging.DEBUG # Send debug logs as events - ) - sentry_sdk.init( - dsn=NUCYPHER_SENTRY_ENDPOINT, - integrations=[sentry_logging], - release='{}@{}'.format(PACKAGE_NAME, __version__) - ) - -# Twisted Log Observer # -######################## - -from nucypher.utilities.logging import simpleObserver -from twisted.logger import globalLogPublisher - -globalLogPublisher.addObserver(simpleObserver) diff --git a/nucypher/config/constants.py b/nucypher/config/constants.py index 8fa142488..2ded4d1e3 100644 --- a/nucypher/config/constants.py +++ b/nucypher/config/constants.py @@ -33,10 +33,7 @@ DEFAULT_CONFIG_ROOT = APP_DIR.user_data_dir SeednodeMetadata = namedtuple('seednode', ['checksum_address', 'rest_host', 'rest_port']) SEEDNODES = tuple() -# Sentry (Set to False to disable sending Errors and Logs to NuCypher's Sentry.) -REPORT_TO_SENTRY = True -NUCYPHER_SENTRY_ENDPOINT = "https://d8af7c4d692e4692a455328a280d845e@sentry.io/1310685" # CLI -DEBUG = True +NUCYPHER_SENTRY_ENDPOINT = "https://d8af7c4d692e4692a455328a280d845e@sentry.io/1310685" KEYRING_PASSPHRASE_ENVVAR_KEY = 'NUCYPHER_KEYRING_PASSPHRASE' diff --git a/nucypher/utilities/logging.py b/nucypher/utilities/logging.py index 3e10c9d1e..b52ced6c6 100644 --- a/nucypher/utilities/logging.py +++ b/nucypher/utilities/logging.py @@ -14,32 +14,33 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with nucypher. If not, see . """ + + +from sentry_sdk import Client, capture_exception, add_breadcrumb from twisted.logger import ILogObserver from zope.interface import provider -from nucypher.config.constants import REPORT_TO_SENTRY + +from nucypher.config.constants import NUCYPHER_SENTRY_ENDPOINT @provider(ILogObserver) def simpleObserver(event): - print(event.get('log_format')) + message = '{} ({}): {}'.format(event.get('log_level').name.upper(), + event.get('log_namespace'), + event.get('log_format')) + print(message) -if REPORT_TO_SENTRY: - from twisted.python import log - from sentry_sdk import Client, capture_exception, add_breadcrumb - from nucypher.config.constants import NUCYPHER_SENTRY_ENDPOINT - +def logToSentry(event): client = Client(dsn=NUCYPHER_SENTRY_ENDPOINT) - def logToSentry(event): + # Handle Logs + if not event.get('isError') or 'failure' not in event: + add_breadcrumb(level=event.get('log_level').name, + message=event.get('log_format'), + category=event.get('log_namespace')) + return - # Handle Logs - if not event.get('isError') or 'failure' not in event: - add_breadcrumb(event) - return - - # Handle Failures - f = event['failure'] - capture_exception((f.type, f.value, f.getTracebackObject())) - - log.addObserver(logToSentry) + # Handle Failures + f = event['failure'] + capture_exception((f.type, f.value, f.getTracebackObject())) diff --git a/tests/conftest.py b/tests/conftest.py index 8b3dc0f29..59b8bd0ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,17 +14,15 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with nucypher. If not, see . """ -from twisted.logger import ILogObserver + +# Test Logger Configuration from twisted.logger import globalLogPublisher -from zope.interface import provider - -@provider(ILogObserver) -def simpleObserver(event): - print(event) +from cli.main import NucypherClickConfig +from nucypher.utilities.logging import simpleObserver +NucypherClickConfig.log_to_sentry = False globalLogPublisher.addObserver(simpleObserver) - """NOTICE: Depends on fixture modules; do not delete""" from .fixtures import * @@ -40,16 +38,6 @@ def pytest_addoption(parser): help="run tests even if they are marked as slow") -def pytest_configure(config): - import sys - sys._pytest_is_running = True - - -def pytest_unconfigure(config): - import sys - del sys._pytest_is_running - - def pytest_collection_modifyitems(config, items): if config.getoption("--runslow"): # --runslow given in cli: do not skip slow tests