diff --git a/nucypher/cli/actions.py b/nucypher/cli/actions.py index a7be07171..c86ca23a6 100644 --- a/nucypher/cli/actions.py +++ b/nucypher/cli/actions.py @@ -11,15 +11,22 @@ from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR from nucypher.network.middleware import RestMiddleware DESTRUCTION = ''' -*Permanently and irreversibly delete all* nucypher files including +*Permanently and irreversibly delete all* nucypher files including: - Private and Public Keys - Known Nodes - TLS certificates - Node Configurations - - Log Files Delete {}?''' +CHARACTER_DESTRUCTION = ''' +Delete all {name} character files including: + - Private and Public Keys + - Known Nodes + - Node Configuration File + +Delete {root}?''' + LOG = Logger('cli.actions') @@ -46,13 +53,12 @@ def load_seednodes(min_stake: int, def destroy_configuration_root(config_root=None, force=False, logs: bool = False) -> str: - """CAUTION: This will destroy *all* nucypher configuration files from the filesystem""" + """CAUTION: This will destroy *all* nucypher configuration files from the configuration root""" config_root = config_root or DEFAULT_CONFIG_ROOT if not force: - message = f"Destroy top-level configuration directory: {config_root}?" - click.confirm(message, abort=True) # ABORT + click.confirm(DESTRUCTION.format(config_root), abort=True) # ABORT if os.path.isdir(config_root): shutil.rmtree(config_root, ignore_errors=force) # config @@ -68,7 +74,8 @@ def destroy_configuration_root(config_root=None, force=False, logs: bool = False def destroy_configuration(character_config, force: bool = False) -> None: if not force: - click.confirm(DESTRUCTION.format(character_config.config_root), abort=True) + click.confirm(CHARACTER_DESTRUCTION.format(name=character_config._NAME, + root=character_config.config_root), abort=True) try: character_config.destroy() @@ -117,3 +124,12 @@ performing accurate re-encryption work orders will result in rewards paid out in ETH retro-actively, on-demand. Accept node operator obligation?""", abort=True) + + +def handle_missing_configuration_file(character_config_class, config_file: str = None): + config_file_location = config_file or character_config_class.DEFAULT_CONFIG_FILE_LOCATION + message = f'No {character_config_class._NAME.capitalize()} configuration file found.\n' \ + f'To create a new persistent {character_config_class._NAME.capitalize()} run: ' \ + f'\'nucypher {character_config_class._NAME} init\'' + + raise click.FileError(filename=config_file_location, hint=message) diff --git a/nucypher/cli/characters/alice.py b/nucypher/cli/characters/alice.py index 849fbccf9..930a6886d 100644 --- a/nucypher/cli/characters/alice.py +++ b/nucypher/cli/characters/alice.py @@ -105,13 +105,17 @@ def alice(click_config, federated_only=True) else: - alice_config = AliceConfiguration.from_configuration_file( - filepath=config_file, - domains={network or GLOBAL_DOMAIN}, - network_middleware=click_config.middleware, - rest_port=discovery_port, - checksum_public_address=checksum_address, - provider_uri=provider_uri) + try: + alice_config = AliceConfiguration.from_configuration_file( + filepath=config_file, + domains={network or GLOBAL_DOMAIN}, + network_middleware=click_config.middleware, + rest_port=discovery_port, + checksum_public_address=checksum_address, + provider_uri=provider_uri) + except FileNotFoundError: + return actions.handle_missing_configuration_file(character_config_class=AliceConfiguration, + config_file=config_file) if not dev: click_config.unlock_keyring(character_configuration=alice_config) diff --git a/nucypher/cli/characters/bob.py b/nucypher/cli/characters/bob.py index 8aa88f9a8..727bd7e98 100644 --- a/nucypher/cli/characters/bob.py +++ b/nucypher/cli/characters/bob.py @@ -62,10 +62,7 @@ def bob(click_config, """Create a brand-new persistent Bob""" if dev: - actions.handle_control_output(message="WARNING: Using temporary storage area", - quiet=quiet, - color='yellow', - json=click_config.json) + click_config.emit(message="WARNING: Using temporary storage area", color='yellow') if not config_root: # Flag config_root = click_config.config_file # Envvar @@ -82,22 +79,6 @@ def bob(click_config, return painting.paint_new_installation_help(new_configuration=new_bob_config, config_file=config_file) - elif action == "destroy": - """Delete all configuration files from the disk""" - - if dev: - message = "'nucypher ursula destroy' cannot be used in --dev mode" - raise click.BadOptionUsage(option_name='--dev', message=message) - - destroyed_path = actions.destroy_configuration_root(config_class=BobConfiguration, - config_file=config_file, - network=network, - config_root=config_root, - force=force) - - return click_config.emitter(message=f"Destroyed {destroyed_path}") - - # # Get Bob Configuration # @@ -109,12 +90,17 @@ def bob(click_config, federated_only=True, network_middleware=click_config.middleware) else: - bob_config = BobConfiguration.from_configuration_file( - filepath=config_file, - domains={network or GLOBAL_DOMAIN}, - rest_port=discovery_port, - provider_uri=provider_uri, - network_middleware=click_config.middleware) + + try: + bob_config = BobConfiguration.from_configuration_file( + filepath=config_file, + domains={network or GLOBAL_DOMAIN}, + rest_port=discovery_port, + provider_uri=provider_uri, + network_middleware=click_config.middleware) + except FileNotFoundError: + return actions.handle_missing_configuration_file(character_config_class=BobConfiguration, + config_file=config_file) # Teacher Ursula teacher_uris = [teacher_uri] if teacher_uri else list() @@ -167,5 +153,12 @@ def bob(click_config, response = BOB.controller.retrieve(request=bob_request_data) return response + elif action == "destroy": + """Delete Bob's character configuration files from the disk""" + if dev: + message = "'nucypher ursula destroy' cannot be used in --dev mode" + raise click.BadOptionUsage(option_name='--dev', message=message) + return actions.destroy_configuration(character_config=bob_config) + else: raise click.BadArgumentUsage(f"No such argument {action}") diff --git a/nucypher/cli/characters/ursula.py b/nucypher/cli/characters/ursula.py index a0c3ea8e2..e8dea4e09 100644 --- a/nucypher/cli/characters/ursula.py +++ b/nucypher/cli/characters/ursula.py @@ -36,7 +36,6 @@ from nucypher.cli.types import ( STAKE_VALUE ) from nucypher.config.characters import UrsulaConfiguration -from nucypher.config.keyring import NucypherKeyring from nucypher.utilities.sandbox.constants import ( TEMPORARY_DOMAIN, ) @@ -197,18 +196,22 @@ def ursula(click_config, else: # Domains -> bytes | or default - domains = [bytes(network, encoding='utf-8')] if network else None + domains = set(bytes(network, encoding='utf-8')) if network else None # Load Ursula from Configuration File - ursula_config = UrsulaConfiguration.from_configuration_file(filepath=config_file, - domains=domains, - registry_filepath=registry_filepath, - provider_uri=provider_uri, - rest_host=rest_host, - rest_port=rest_port, - db_filepath=db_filepath, - poa=poa, - federated_only=federated_only) + try: + ursula_config = UrsulaConfiguration.from_configuration_file(filepath=config_file, + domains=domains, + registry_filepath=registry_filepath, + provider_uri=provider_uri, + rest_host=rest_host, + rest_port=rest_port, + db_filepath=db_filepath, + poa=poa, + federated_only=federated_only) + except FileNotFoundError: + return actions.handle_missing_configuration_file(character_config_class=UrsulaConfiguration, + config_file=config_file) click_config.unlock_keyring(character_configuration=ursula_config) diff --git a/nucypher/config/keyring.py b/nucypher/config/keyring.py index 7393a7a6d..50f96239b 100644 --- a/nucypher/config/keyring.py +++ b/nucypher/config/keyring.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ import base64 +import contextlib import json import os import shutil @@ -666,8 +667,8 @@ class NucypherKeyring: keypaths = self._generate_key_filepaths(account=self.checksum_address, public_key_dir=public_key_dir, private_key_dir=private_key_dir) + + # Remove the parsed paths from the disk, weather they exist or not. for filepath in keypaths.values(): - try: + with contextlib.suppress(FileNotFoundError): os.remove(filepath) - except FileNotFoundError: - pass \ No newline at end of file diff --git a/tests/cli/test_mixed_configurations.py b/tests/cli/test_mixed_configurations.py index 365c65e8b..fb4555963 100644 --- a/tests/cli/test_mixed_configurations.py +++ b/tests/cli/test_mixed_configurations.py @@ -81,7 +81,7 @@ def test_coexisting_configurations(click_runner, assert os.path.isfile(felix_file_location) assert len(os.listdir(public_keys_dir)) == 3 - # Use a custom local filepath to init an persistent Alice + # Use a custom local filepath to init a persistent Alice alice_init_args = ('alice', 'init', '--network', TEMPORARY_DOMAIN, '--provider-uri', TEST_PROVIDER_URI, @@ -94,7 +94,7 @@ def test_coexisting_configurations(click_runner, assert os.path.isfile(alice_file_location) assert len(os.listdir(public_keys_dir)) == 5 - # Use the same local filepath to init an persistent Ursula + # Use the same local filepath to init a persistent Ursula init_args = ('ursula', 'init', '--network', TEMPORARY_DOMAIN, '--provider-uri', TEST_PROVIDER_URI, @@ -128,15 +128,16 @@ def test_coexisting_configurations(click_runner, # Run # - # Now start running your Ursula! + # Run an Ursula amidst the other configuration files run_args = ('ursula', 'run', '--dry-run', '--config-file', another_ursula_configuration_file_location) user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' result = click_runner.invoke(nucypher_cli, run_args, input=user_input, catch_exceptions=False) assert result.exit_code == 0 - assert another_ursula in result.output + # Check that the proper Ursula console is attached + assert another_ursula in result.output # # Destroy diff --git a/tests/cli/ursula/test_federated_ursula.py b/tests/cli/ursula/test_federated_ursula.py index 645a2005f..26371da00 100644 --- a/tests/cli/ursula/test_federated_ursula.py +++ b/tests/cli/ursula/test_federated_ursula.py @@ -220,7 +220,7 @@ def test_ursula_destroy_configuration(custom_filepath, click_runner): # Ensure the files are deleted from the filesystem assert not os.path.isfile(custom_config_filepath), 'Files still exist' # ... shes's gone... - assert os.path.isdir(custom_filepath), 'Nucypher files no longer still exist' # ... but not NuCypher ... + assert os.path.isdir(custom_filepath), 'Nucypher files no longer exist' # ... but not NuCypher ... # If this test started off with a live configuration, ensure it still exists if preexisting_live_configuration: