Push common learner configuration logic down to NodeStorage; Pre and Post character production logic, extracted keyring unlock UX.

pull/562/head
Kieran Prasch 2018-11-22 10:11:11 -08:00
parent 305eadc8d8
commit e343a675ac
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
2 changed files with 296 additions and 213 deletions

View File

@ -18,85 +18,35 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import os import os
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.x509 import Certificate
from web3.middleware import geth_poa_middleware from web3.middleware import geth_poa_middleware
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, CONTRACT_NOT_AVAILABLE from constant_sorrow.constants import (
UNINITIALIZED_CONFIGURATION,
NO_KEYRING_ATTACHED
)
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent
from nucypher.blockchain.eth.chains import Blockchain from nucypher.blockchain.eth.chains import Blockchain
from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.config.node import NodeConfiguration from nucypher.config.node import NodeConfiguration
from nucypher.crypto.powers import CryptoPower
class UrsulaConfiguration(NodeConfiguration): class UrsulaConfiguration(NodeConfiguration):
from nucypher.characters.lawful import Ursula from nucypher.characters.lawful import Ursula
_character_class = Ursula _CHARACTER_CLASS = Ursula
_name = 'ursula' _NAME = 'ursula'
DEFAULT_DB_FILEPATH = os.path.join(DEFAULT_CONFIG_ROOT, '{}.db'.format(_name)) CONFIG_FILENAME = '{}.config'.format(_NAME)
DEFAULT_REST_HOST = '127.0.0.1' DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)
DEFAULT_REST_PORT = 9151 DEFAULT_DB_NAME = '{}.db'.format(_NAME)
__DEFAULT_TLS_CURVE = ec.SECP384R1
def __init__(self,
rest_host: str = None,
rest_port: int = None,
# TLS
tls_curve: EllipticCurve = None,
certificate: Certificate = None,
certificate_filepath: str = None,
# Ursula
db_filepath: str = None,
interface_signature=None,
crypto_power: CryptoPower = None,
# Blockchain
poa: bool = False,
provider_uri: str = None,
*args, **kwargs
) -> None:
# REST
self.rest_host = rest_host or self.DEFAULT_REST_HOST
self.rest_port = rest_port or self.DEFAULT_REST_PORT
self.db_filepath = db_filepath or self.DEFAULT_DB_FILEPATH
#
# TLS
#
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
self.certificate = certificate
self.certificate_filepath = certificate_filepath
# Ursula
self.interface_signature = interface_signature
self.crypto_power = crypto_power
#
# Blockchain
#
self.poa = poa
self.provider_uri = provider_uri
self.blockchain = NO_BLOCKCHAIN_CONNECTION
self.token_agent = CONTRACT_NOT_AVAILABLE
self.miner_agent = CONTRACT_NOT_AVAILABLE
self.policy_agent = CONTRACT_NOT_AVAILABLE
def __init__(self, db_filepath: str = None, *args, **kwargs) -> None:
self.db_filepath = db_filepath or UNINITIALIZED_CONFIGURATION
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def generate_runtime_filepaths(self, config_root: str) -> dict: def generate_runtime_filepaths(self, config_root: str) -> dict:
base_filepaths = NodeConfiguration.generate_runtime_filepaths(config_root=config_root) base_filepaths = super().generate_runtime_filepaths(config_root=config_root)
filepaths = dict(db_filepath=os.path.join(config_root, self.db_filepath)) filepaths = dict(db_filepath=os.path.join(config_root, self.DEFAULT_DB_NAME))
base_filepaths.update(filepaths) base_filepaths.update(filepaths)
return base_filepaths return base_filepaths
@ -107,36 +57,40 @@ class UrsulaConfiguration(NodeConfiguration):
rest_port=self.rest_port, rest_port=self.rest_port,
db_filepath=self.db_filepath, db_filepath=self.db_filepath,
) )
if not self.dev:
certificate_filepath = self.certificate_filepath or self.keyring.certificate_filepath
payload.update(dict(certificate_filepath=certificate_filepath))
return {**super().static_payload, **payload} return {**super().static_payload, **payload}
@property @property
def dynamic_payload(self) -> dict: def dynamic_payload(self) -> dict:
payload = dict( payload = dict(
network_middleware=self.network_middleware, network_middleware=self.network_middleware,
tls_curve=self.tls_curve, # TODO: Needs to be in static payload with mapping tls_curve=self.tls_curve, # TODO: Needs to be in static payload with [str -> curve] mapping
certificate=self.certificate, certificate=self.certificate,
interface_signature=self.interface_signature, interface_signature=self.interface_signature,
timestamp=None, timestamp=None,
) )
return {**super().dynamic_payload, **payload} return {**super().dynamic_payload, **payload}
def produce(self, password: str = None, **overrides): def produce(self, **overrides):
"""Produce a new Ursula from configuration""" """Produce a new Ursula from configuration"""
if not self.dev: # Build a merged dict of Ursula parameters
self.read_keyring()
self.keyring.unlock(password=password)
merged_parameters = {**self.static_payload, **self.dynamic_payload, **overrides} merged_parameters = {**self.static_payload, **self.dynamic_payload, **overrides}
#
# Pre-Init
#
# Verify the configuration file refers to the same configuration root as this instance
config_root_from_config_file = merged_parameters.pop('config_root')
if config_root_from_config_file != self.config_root:
message = "Configuration root mismatch {} and {}.".format(config_root_from_config_file, self.config_root)
raise self.ConfigurationError(message)
if self.federated_only is False: if self.federated_only is False:
self.blockchain = Blockchain.connect(provider_uri=self.provider_uri) self.blockchain = Blockchain.connect(provider_uri=self.provider_uri)
if self.poa: # TODO: move this..? if self.poa:
w3 = self.miner_agent.blockchain.interface.w3 w3 = self.miner_agent.blockchain.interface.w3
w3.middleware_stack.inject(geth_poa_middleware, layer=0) w3.middleware_stack.inject(geth_poa_middleware, layer=0)
@ -144,9 +98,15 @@ class UrsulaConfiguration(NodeConfiguration):
self.miner_agent = MinerAgent(blockchain=self.blockchain) self.miner_agent = MinerAgent(blockchain=self.blockchain)
merged_parameters.update(blockchain=self.blockchain) merged_parameters.update(blockchain=self.blockchain)
ursula = self._character_class(**merged_parameters) #
# Init
#
ursula = self._CHARACTER_CLASS(**merged_parameters)
if self.dev: # TODO: Move this..? #
# Post-Init
#
if self.dev_mode:
class MockDatastoreThreadPool(object): class MockDatastoreThreadPool(object):
def callInThread(self, f, *args, **kwargs): def callInThread(self, f, *args, **kwargs):
return f(*args, **kwargs) return f(*args, **kwargs)
@ -154,15 +114,33 @@ class UrsulaConfiguration(NodeConfiguration):
return ursula return ursula
def __write(self, password: str, no_registry: bool):
_new_installation_path = self.initialize(password=password, import_registry=no_registry)
_configuration_filepath = self.to_configuration_file(filepath=self.config_file_location)
@classmethod
def generate(cls, password: str, no_registry: bool, *args, **kwargs) -> 'UrsulaConfiguration':
"""Hook-up a new initial installation and write configuration file to the disk"""
ursula_config = cls(dev_mode=False, is_me=True, *args, **kwargs)
ursula_config.__write(password=password, no_registry=no_registry)
return ursula_config
class AliceConfiguration(NodeConfiguration): class AliceConfiguration(NodeConfiguration):
from nucypher.characters.lawful import Alice from nucypher.characters.lawful import Alice
_character_class = Alice _CHARACTER_CLASS = Alice
_name = 'alice' _NAME = 'alice'
CONFIG_FILENAME = '{}.config'.format(_NAME)
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)
class BobConfiguration(NodeConfiguration): class BobConfiguration(NodeConfiguration):
from nucypher.characters.lawful import Bob from nucypher.characters.lawful import Bob
_character_class = Bob
_name = 'bob' _CHARACTER_CLASS = Bob
_NAME = 'bob'
CONFIG_FILENAME = '{}.config'.format(_NAME)
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)

View File

@ -19,44 +19,68 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import binascii import binascii
import json import json
import os import os
import secrets
import string
from abc import ABC, abstractmethod
from json import JSONDecodeError from json import JSONDecodeError
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import shutil
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.x509 import Certificate
from twisted.logger import Logger
from typing import List from typing import List
from web3.middleware import geth_poa_middleware from web3.middleware import geth_poa_middleware
from constant_sorrow.constants import UNINITIALIZED_CONFIGURATION, STRANGER_CONFIGURATION, LIVE_CONFIGURATION from constant_sorrow.constants import (
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve UNINITIALIZED_CONFIGURATION,
from twisted.logger import Logger STRANGER_CONFIGURATION,
NO_BLOCKCHAIN_CONNECTION,
LIVE_CONFIGURATION,
NO_KEYRING_ATTACHED
)
from nucypher.blockchain.eth.agents import PolicyAgent, MinerAgent, NucypherTokenAgent from nucypher.blockchain.eth.agents import PolicyAgent, MinerAgent, NucypherTokenAgent
from nucypher.blockchain.eth.chains import Blockchain from nucypher.blockchain.eth.chains import Blockchain
from nucypher.characters.lawful import Ursula from nucypher.config.constants import DEFAULT_CONFIG_ROOT, BASE_DIR, USER_LOG_DIR
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, BASE_DIR
from nucypher.config.keyring import NucypherKeyring from nucypher.config.keyring import NucypherKeyring
from nucypher.config.storages import NodeStorage, ForgetfulNodeStorage, LocalFileBasedNodeStorage from nucypher.config.storages import NodeStorage, ForgetfulNodeStorage, LocalFileBasedNodeStorage
from nucypher.crypto.powers import CryptoPowerUp from nucypher.crypto.powers import CryptoPowerUp, CryptoPower
from nucypher.network.middleware import RestMiddleware from nucypher.network.middleware import RestMiddleware
from umbral.signing import Signature
class NodeConfiguration: class NodeConfiguration(ABC):
"""
'Sideways Engagement' of Character classes; a reflection of input parameters.
"""
_name = 'ursula' # Abstract
_character_class = Ursula _NAME = NotImplemented
_CHARACTER_CLASS = NotImplemented
CONFIG_FILENAME = NotImplemented
DEFAULT_CONFIG_FILE_LOCATION = NotImplemented
CONFIG_FILENAME = '{}.config'.format(_name) # Mode
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)
DEFAULT_OPERATING_MODE = 'decentralized' DEFAULT_OPERATING_MODE = 'decentralized'
NODE_SERIALIZER = binascii.hexlify NODE_SERIALIZER = binascii.hexlify
NODE_DESERIALIZER = binascii.unhexlify NODE_DESERIALIZER = binascii.unhexlify
# Configuration
__CONFIG_FILE_EXT = '.config' __CONFIG_FILE_EXT = '.config'
__CONFIG_FILE_DESERIALIZER = json.loads __CONFIG_FILE_DESERIALIZER = json.loads
__TEMP_CONFIGURATION_DIR_PREFIX = "nucypher-tmp-" __TEMP_CONFIGURATION_DIR_PREFIX = "nucypher-tmp-"
__DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
# Registry
__REGISTRY_NAME = 'contract_registry.json' __REGISTRY_NAME = 'contract_registry.json'
REGISTRY_SOURCE = os.path.join(BASE_DIR, __REGISTRY_NAME) # TODO: #461 Where will this be hosted? REGISTRY_SOURCE = os.path.join(BASE_DIR, __REGISTRY_NAME) # TODO: #461 Where will this be hosted?
# Rest + TLS
DEFAULT_REST_HOST = '127.0.0.1'
DEFAULT_REST_PORT = 9151
__DEFAULT_TLS_CURVE = ec.SECP384R1
__DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
class ConfigurationError(RuntimeError): class ConfigurationError(RuntimeError):
pass pass
@ -65,90 +89,122 @@ class NodeConfiguration:
def __init__(self, def __init__(self,
dev: bool = False, # Mode
config_root: str = None, dev_mode: bool = False,
federated_only: bool = False,
password: str = None, # Keyring
auto_initialize: bool = False,
auto_generate_keys: bool = False,
config_file_location: str = None,
keyring: NucypherKeyring = None, keyring: NucypherKeyring = None,
keyring_dir: str = None, keyring_dir: str = None,
checksum_address: str = None, # Identity
is_me: bool = True, is_me: bool = True,
federated_only: bool = False, checksum_address: str = None,
network_middleware: RestMiddleware = None,
registry_source: str = None,
registry_filepath: str = None,
import_seed_registry: bool = False,
# Learner # Learner
learn_on_same_thread: bool = False, learn_on_same_thread: bool = False,
abort_on_learning_error: bool = False, abort_on_learning_error: bool = False,
start_learning_now: bool = True, start_learning_now: bool = True,
# Configuration
config_root: str = None,
config_file_location: str = None,
# REST + TLS
rest_host: str = None,
rest_port: int = None,
tls_curve: EllipticCurve = None,
certificate: Certificate = None,
crypto_power: CryptoPower = None,
interface_signature: Signature = None,
network_middleware: RestMiddleware = None,
# Node Storage # Node Storage
known_nodes: set = None, known_nodes: set = None,
node_storage: NodeStorage = None, node_storage: NodeStorage = None,
load_metadata: bool = True, load_metadata: bool = True,
save_metadata: bool = True, save_metadata: bool = True,
# Blockchain
poa: bool = False,
provider_uri: str = None,
# Registry
registry_source: str = None,
registry_filepath: str = None,
import_seed_registry: bool = False # TODO: needs cleanup
) -> None: ) -> None:
# Logs # Logs
self.log = Logger(self.__class__.__name__) self.log = Logger(self.__class__.__name__)
#
# REST + TLS
#
self.rest_host = rest_host or self.DEFAULT_REST_HOST
self.rest_port = rest_port or self.DEFAULT_REST_PORT
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
self.certificate = certificate
self.interface_signature = interface_signature
self.crypto_power = crypto_power
#
# Keyring # Keyring
self.keyring = keyring or UNINITIALIZED_CONFIGURATION #
self.keyring = keyring or NO_KEYRING_ATTACHED
self.keyring_dir = keyring_dir or UNINITIALIZED_CONFIGURATION self.keyring_dir = keyring_dir or UNINITIALIZED_CONFIGURATION
# Contract Registry # Contract Registry
if import_seed_registry is True:
registry_source = self.REGISTRY_SOURCE
if not os.path.isfile(registry_source):
message = "Seed contract registry does not exist at path {}.".format(registry_filepath)
self.log.debug(message)
raise RuntimeError(message)
self.__registry_source = registry_source or self.REGISTRY_SOURCE self.__registry_source = registry_source or self.REGISTRY_SOURCE
self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION
# Configuration File and Root Directory #
# Configuration
#
self.config_file_location = config_file_location or UNINITIALIZED_CONFIGURATION self.config_file_location = config_file_location or UNINITIALIZED_CONFIGURATION
self.config_root = UNINITIALIZED_CONFIGURATION self.config_root = UNINITIALIZED_CONFIGURATION
self.__dev = dev
if self.__dev: #
# Mode
#
self.federated_only = federated_only
self.__dev_mode = dev_mode
if self.__dev_mode:
self.__temp_dir = UNINITIALIZED_CONFIGURATION self.__temp_dir = UNINITIALIZED_CONFIGURATION
self.node_storage = ForgetfulNodeStorage(federated_only=federated_only, character_class=self.__class__) self.node_storage = ForgetfulNodeStorage(federated_only=federated_only, character_class=self.__class__)
else: else:
from nucypher.characters.lawful import Ursula # TODO : Needs cleanup
self.__temp_dir = LIVE_CONFIGURATION self.__temp_dir = LIVE_CONFIGURATION
self.config_root = config_root or DEFAULT_CONFIG_ROOT self.config_root = config_root or DEFAULT_CONFIG_ROOT
self.__cache_runtime_filepaths() self._cache_runtime_filepaths()
self.node_storage = node_storage or LocalFileBasedNodeStorage(federated_only=federated_only, self.node_storage = node_storage or LocalFileBasedNodeStorage(federated_only=federated_only,
config_root=self.config_root) config_root=self.config_root)
# #
# Identity # Identity
# #
self.federated_only = federated_only
self.checksum_address = checksum_address
self.is_me = is_me self.is_me = is_me
if self.is_me: self.checksum_address = checksum_address
#
if self.is_me is True or dev_mode is True:
# Self # Self
# if self.checksum_address and dev_mode is False:
if checksum_address and not self.__dev: self.attach_keyring()
self.read_keyring()
self.network_middleware = network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS() self.network_middleware = network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS()
else: else:
#
# Stranger # Stranger
#
self.node_storage = STRANGER_CONFIGURATION self.node_storage = STRANGER_CONFIGURATION
self.keyring_dir = STRANGER_CONFIGURATION self.keyring_dir = STRANGER_CONFIGURATION
self.keyring = STRANGER_CONFIGURATION self.keyring = STRANGER_CONFIGURATION
self.network_middleware = STRANGER_CONFIGURATION self.network_middleware = STRANGER_CONFIGURATION
if network_middleware: if network_middleware:
raise self.ConfigurationError("Cannot configure a stranger to use network middleware") raise self.ConfigurationError("Cannot configure a stranger to use network middleware.")
# #
# Learner # Learner
@ -161,53 +217,127 @@ class NodeConfiguration:
self.load_metadata = load_metadata self.load_metadata = load_metadata
# #
# Auto-Initialization # Blockchain
# #
if auto_initialize: self.poa = poa
self.initialize(no_registry=not import_seed_registry or federated_only, # TODO: needs cleanup self.provider_uri = provider_uri
wallet=auto_generate_keys and not federated_only,
encrypting=auto_generate_keys, self.blockchain = NO_BLOCKCHAIN_CONNECTION
password=password) self.accounts = NO_BLOCKCHAIN_CONNECTION
self.token_agent = NO_BLOCKCHAIN_CONNECTION
self.miner_agent = NO_BLOCKCHAIN_CONNECTION
self.policy_agent = NO_BLOCKCHAIN_CONNECTION
#
# Development Mode
#
if dev_mode:
# Ephemeral dev settings
self.save_metadata = False
self.load_metadata = False
# Generate one-time alphanumeric development password
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for _ in range(32))
self.initialize(password=password, import_registry=import_seed_registry)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.produce(*args, **kwargs) return self.produce(*args, **kwargs)
def cleanup(self) -> None: def cleanup(self) -> None:
if self.__dev: if self.__dev_mode:
self.__temp_dir.cleanup() self.__temp_dir.cleanup()
@property @property
def dev(self): def dev_mode(self):
return self.__dev return self.__dev_mode
def produce(self, password: str = None, **overrides): def connect_to_blockchain(self, provider_uri: str, poa: bool, compile: bool):
"""Initialize a new character instance and return it""" if self.federated_only:
if not self.dev: raise NodeConfiguration.ConfigurationError("Cannot connect to blockchain in federated mode")
self.read_keyring()
self.keyring.unlock(password=password) self.blockchain = Blockchain.connect(provider_uri=provider_uri, compile=compile)
if poa is True:
w3 = self.blockchain.interface.w3
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
self.accounts = self.blockchain.interface.w3.eth.accounts
self.log.debug("Established connection to provider {}".format(self.blockchain.interface.provider_uri))
def connect_to_contracts(self) -> None:
"""Initialize contract agency and set them on config"""
self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
self.miner_agent = MinerAgent(blockchain=self.blockchain)
self.policy_agent = PolicyAgent(blockchain=self.blockchain)
self.log.debug("Established connection to nucypher contracts")
def read_known_nodes(self):
self.known_nodes.update(self.node_storage.all(federated_only=self.federated_only))
return self.known_nodes
def forget_nodes(self) -> None:
self.node_storage.clear()
message = "Removed all stored node node metadata and certificates"
self.log.debug(message)
def destroy(self, force: bool = False, logs: bool = True) -> None:
# TODO: Confirm this is a nucypher dir first! (in-depth measure)
if force:
pass
if logs is True:
shutil.rmtree(USER_LOG_DIR)
try:
shutil.rmtree(self.config_root)
except FileNotFoundError:
raise
def produce(self, **overrides):
"""Initialize a new character instance and return it."""
# Build a merged dict of node parameters
merged_parameters = {**self.static_payload, **self.dynamic_payload, **overrides} merged_parameters = {**self.static_payload, **self.dynamic_payload, **overrides}
return self._character_class(**merged_parameters)
# Verify the configuration file refers to the same configuration root as this instance
config_root_from_config_file = merged_parameters.pop('config_root')
if config_root_from_config_file != self.config_root:
message = "Configuration root mismatch {} and {}.".format(config_root_from_config_file, self.config_root)
raise self.ConfigurationError(message)
character = self._CHARACTER_CLASS(**merged_parameters)
return character
@staticmethod @staticmethod
def _read_configuration_file(filepath) -> dict: def _read_configuration_file(filepath: str) -> dict:
with open(filepath, 'r') as file: try:
payload = NodeConfiguration.__CONFIG_FILE_DESERIALIZER(file.read()) with open(filepath, 'r') as file:
raw_contents = file.read()
payload = NodeConfiguration.__CONFIG_FILE_DESERIALIZER(raw_contents)
except FileNotFoundError as e:
raise # TODO: Do we need better exception handling here?
return payload return payload
@classmethod @classmethod
def from_configuration_file(cls, filepath, **overrides) -> 'NodeConfiguration': def from_configuration_file(cls, filepath: str = None, **overrides) -> 'NodeConfiguration':
"""Initialize a NodeConfiguration from a JSON file.""" """Initialize a NodeConfiguration from a JSON file."""
from nucypher.config.storages import NodeStorage # TODO: move
NODE_STORAGES = {storage_class._name: storage_class for storage_class in NodeStorage.__subclasses__()}
from nucypher.config.storages import NodeStorage
node_storage_subclasses = {storage._name: storage for storage in NodeStorage.__subclasses__()}
if filepath is None:
filepath = cls.DEFAULT_CONFIG_FILE_LOCATION
# Read from disk
payload = cls._read_configuration_file(filepath=filepath) payload = cls._read_configuration_file(filepath=filepath)
# Make NodeStorage # Initialize NodeStorage subclass from file (sub-configuration)
storage_payload = payload['node_storage'] storage_payload = payload['node_storage']
storage_type = storage_payload[NodeStorage._TYPE_LABEL] storage_type = storage_payload[NodeStorage._TYPE_LABEL]
storage_class = NODE_STORAGES[storage_type] storage_class = node_storage_subclasses[storage_type]
node_storage = storage_class.from_payload(payload=storage_payload, node_storage = storage_class.from_payload(payload=storage_payload,
character_class=cls._character_class, character_class=cls._CHARACTER_CLASS,
federated_only=payload['federated_only'], federated_only=payload['federated_only'],
serializer=cls.NODE_SERIALIZER, serializer=cls.NODE_SERIALIZER,
deserializer=cls.NODE_DESERIALIZER) deserializer=cls.NODE_DESERIALIZER)
@ -218,11 +348,12 @@ class NodeConfiguration:
def to_configuration_file(self, filepath: str = None) -> str: def to_configuration_file(self, filepath: str = None) -> str:
"""Write the static_payload to a JSON file.""" """Write the static_payload to a JSON file."""
if filepath is None: if filepath is None:
filename = '{}{}'.format(self._name.lower(), self.__CONFIG_FILE_EXT) filename = '{}{}'.format(self._NAME.lower(), self.__CONFIG_FILE_EXT)
filepath = os.path.join(self.config_root, filename) filepath = os.path.join(self.config_root, filename)
payload = self.static_payload payload = self.static_payload
del payload['is_me'] # TODO del payload['is_me'] # TODO
# Save node connection data # Save node connection data
payload.update(dict(node_storage=self.node_storage.payload())) payload.update(dict(node_storage=self.node_storage.payload()))
@ -250,6 +381,8 @@ class NodeConfiguration:
def static_payload(self) -> dict: def static_payload(self) -> dict:
"""Exported static configuration values for initializing Ursula""" """Exported static configuration values for initializing Ursula"""
payload = dict( payload = dict(
config_root=self.config_root,
# Identity # Identity
is_me=self.is_me, is_me=self.is_me,
federated_only=self.federated_only, # TODO: 466 federated_only=self.federated_only, # TODO: 466
@ -294,7 +427,7 @@ class NodeConfiguration:
registry_filepath=os.path.join(config_root, NodeConfiguration.__REGISTRY_NAME)) registry_filepath=os.path.join(config_root, NodeConfiguration.__REGISTRY_NAME))
return filepaths return filepaths
def __cache_runtime_filepaths(self) -> None: def _cache_runtime_filepaths(self) -> None:
"""Generate runtime filepaths and cache them on the config object""" """Generate runtime filepaths and cache them on the config object"""
filepaths = self.generate_runtime_filepaths(config_root=self.config_root) filepaths = self.generate_runtime_filepaths(config_root=self.config_root)
for field, filepath in filepaths.items(): for field, filepath in filepaths.items():
@ -303,28 +436,22 @@ class NodeConfiguration:
def derive_node_power_ups(self) -> List[CryptoPowerUp]: def derive_node_power_ups(self) -> List[CryptoPowerUp]:
power_ups = list() power_ups = list()
if self.is_me and not self.dev: if self.is_me and not self.dev_mode:
for power_class in self._character_class._default_crypto_powerups: for power_class in self._CHARACTER_CLASS._default_crypto_powerups:
power_up = self.keyring.derive_crypto_power(power_class) power_up = self.keyring.derive_crypto_power(power_class)
power_ups.append(power_up) power_ups.append(power_up)
return power_ups return power_ups
def initialize(self, def initialize(self,
password: str, password: str,
no_registry: bool = False, import_registry: bool = True,
wallet: bool = False,
encrypting: bool = False,
tls: bool = False,
host: str = None,
curve=None,
no_keys: bool = False
) -> str: ) -> str:
"""Initialize a new configuration.""" """Initialize a new configuration."""
# #
# Create Config Root # Create Config Root
# #
if self.__dev: if self.__dev_mode:
self.__temp_dir = TemporaryDirectory(prefix=self.__TEMP_CONFIGURATION_DIR_PREFIX) self.__temp_dir = TemporaryDirectory(prefix=self.__TEMP_CONFIGURATION_DIR_PREFIX)
self.config_root = self.__temp_dir.name self.config_root = self.__temp_dir.name
else: else:
@ -340,57 +467,56 @@ class NodeConfiguration:
# #
# Create Config Subdirectories # Create Config Subdirectories
# #
self.__cache_runtime_filepaths() self._cache_runtime_filepaths()
try: try:
# Node Storage # Node Storage
self.node_storage.initialize() self.node_storage.initialize()
# Keyring # Keyring
os.mkdir(self.keyring_dir, mode=0o700) # keyring TODO: Keyring backend entry point if not self.dev_mode:
if not self.dev and not no_keys: os.mkdir(self.keyring_dir, mode=0o700) # keyring TODO: Keyring backend entry point
# Keyring self.write_keyring(password=password, host=self.rest_host, tls_curve=self.tls_curve)
self.write_keyring(password=password,
wallet=wallet,
encrypting=encrypting,
tls=tls,
host=host,
tls_curve=curve)
# Registry # Registry
if not no_registry and not self.federated_only: if import_registry and not self.federated_only:
self.write_registry(output_filepath=self.registry_filepath, self.write_registry(output_filepath=self.registry_filepath, # type: str
source=self.__registry_source, source=self.__registry_source, # type: str
blank=no_registry) blank=import_registry) # type: bool
except FileExistsError: except FileExistsError:
existing_paths = [os.path.join(self.config_root, f) for f in os.listdir(self.config_root)] existing_paths = [os.path.join(self.config_root, f) for f in os.listdir(self.config_root)]
message = "There are pre-existing nucypher installation files at {}: {}".format(self.config_root, message = "There are pre-existing files at {}: {}".format(self.config_root, existing_paths)
existing_paths)
self.log.critical(message) self.log.critical(message)
raise NodeConfiguration.ConfigurationError(message) raise NodeConfiguration.ConfigurationError(message)
if not self.__dev: if not self.__dev_mode:
self.validate(config_root=self.config_root, no_registry=no_registry or self.federated_only) self.validate(config_root=self.config_root, no_registry=import_registry or self.federated_only)
# Success
message = "Created nucypher installation files at {}".format(self.config_root)
self.log.debug(message)
return self.config_root return self.config_root
def read_known_nodes(self): def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None:
self.known_nodes.update(self.node_storage.all(federated_only=self.federated_only)) if self.keyring is not NO_KEYRING_ATTACHED:
return self.known_nodes if self.keyring.checksum_address != (checksum_address or self.checksum_address):
raise self.ConfigurationError("There is already a keyring attached to this configuration.")
return
def read_keyring(self, *args, **kwargs): if (checksum_address or self.checksum_address) is None:
if self.checksum_address is None:
raise self.ConfigurationError("No account specified to unlock keyring") raise self.ConfigurationError("No account specified to unlock keyring")
self.keyring = NucypherKeyring(keyring_root=self.keyring_dir,
account=self.checksum_address, self.keyring = NucypherKeyring(keyring_root=self.keyring_dir, # type: str
account=checksum_address or self.checksum_address, # type: str
*args, **kwargs) *args, **kwargs)
def write_keyring(self, def write_keyring(self,
password: str,
encrypting: bool,
wallet: bool,
tls: bool,
host: str, host: str,
password: str,
encrypting: bool = True,
wallet: bool = False,
tls: bool = True,
tls_curve: EllipticCurve = None, tls_curve: EllipticCurve = None,
) -> NucypherKeyring: ) -> NucypherKeyring:
@ -407,8 +533,6 @@ class NodeConfiguration:
self.checksum_address = self.keyring.federated_address self.checksum_address = self.keyring.federated_address
else: else:
self.checksum_address = self.keyring.checksum_address self.checksum_address = self.keyring.checksum_address
if tls:
self.certificate_filepath = self.keyring.certificate_filepath
return self.keyring return self.keyring
@ -425,7 +549,7 @@ class NodeConfiguration:
output_filepath = output_filepath or self.registry_filepath output_filepath = output_filepath or self.registry_filepath
source = source or self.REGISTRY_SOURCE source = source or self.REGISTRY_SOURCE
if not blank and not self.dev: if not blank and not self.dev_mode:
# Validate Registry # Validate Registry
with open(source, 'r') as registry_file: with open(source, 'r') as registry_file:
try: try:
@ -443,22 +567,3 @@ class NodeConfiguration:
self.log.debug("Successfully wrote registry to {}".format(output_filepath)) self.log.debug("Successfully wrote registry to {}".format(output_filepath))
return output_filepath return output_filepath
def connect_to_blockchain(self, provider_uri: str, poa: bool, compile: bool):
if self.federated_only:
raise NodeConfiguration.ConfigurationError("Cannot connect to blockchain in federated mode")
self.blockchain = Blockchain.connect(provider_uri=provider_uri, compile=compile)
if poa is True:
w3 = self.blockchain.interface.w3
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
self.accounts = self.blockchain.interface.w3.eth.accounts
self.log.debug("Established connection to provider {}".format(self.blockchain.interface.provider_uri))
def connect_to_contracts(self) -> None:
"""Initialize contract agency and set them on config"""
self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
self.miner_agent = MinerAgent(blockchain=self.blockchain)
self.policy_agent = PolicyAgent(blockchain=self.blockchain)
self.log.debug("CLI established connection to nucypher contracts")