Reflect character initialization logic in NodeConfiguration subclasses; Integrate with local filesystem paths in dev and non-dev modes.

pull/447/head
Kieran Prasch 2018-09-22 15:43:35 -07:00
parent f7b99df465
commit 13764517ac
8 changed files with 160 additions and 145 deletions

View File

@ -12,6 +12,7 @@ import os
import maya
from nucypher.characters.lawful import Alice, Bob, Ursula
from nucypher.config.characters import AliceConfiguration
from nucypher.data_sources import DataSource
# This is already running in another process.
from nucypher.network.middleware import RestMiddleware
@ -51,12 +52,12 @@ URSULA.save_certificate_to_disk(CERTIFICATE_DIR)
#########
ALICE = Alice(network_middleware=RestMiddleware(),
known_nodes=(URSULA,),
federated_only=True,
always_be_learning=True,
known_certificates_dir=CERTIFICATE_DIR,
) # TODO: 289
ALICE = AliceConfiguration(network_middleware=RestMiddleware(),
known_nodes=(URSULA,),
federated_only=True,
always_be_learning=True,
known_certificates_dir=CERTIFICATE_DIR,
) # TODO: 289
# Here are our Policy details.
policy_end_datetime = maya.now() + datetime.timedelta(days=5)

View File

@ -34,7 +34,6 @@ CERTIFICATE_DIR = "{}/certs".format(CRUFTSPACE)
def spin_up_ursula(rest_port, db_name, teachers=(), certificate_dir=None):
metadata_file = "examples-runtime-cruft/node-metadata-{}".format(rest_port)
asyncio.set_event_loop(asyncio.new_event_loop()) # Ugh. Awful. But needed until we shed the DHT.
_URSULA = Ursula(rest_port=rest_port,
rest_host="localhost",
db_name=db_name,

View File

@ -1,6 +1,4 @@
import os
from glob import glob
from os.path import abspath
from constant_sorrow import constants
from cryptography.hazmat.primitives.asymmetric import ec
@ -15,11 +13,12 @@ from nucypher.crypto.powers import CryptoPower
class UrsulaConfiguration(NodeConfiguration):
DEFAULT_TLS_CURVE = ec.SECP384R1
DEFAULT_REST_HOST = 'localhost'
DEFAULT_REST_PORT = 9151
DEFAULT_DB_TEMPLATE = "ursula.{port}.db"
__REGISTRY_NAME = 'contract_registry.json'
__DB_TEMPLATE = "ursula.{port}.db"
DEFAULT_DB_NAME = __DB_TEMPLATE.format(port=DEFAULT_REST_PORT)
__DEFAULT_TLS_CURVE = ec.SECP384R1
def __init__(self,
rest_host: str = None,
@ -29,7 +28,6 @@ class UrsulaConfiguration(NodeConfiguration):
tls_curve: EllipticCurve = None,
tls_private_key: bytes = None,
certificate: Certificate = None,
certificate_filepath: str = None,
# Ursula
db_name: str = None,
@ -40,7 +38,6 @@ class UrsulaConfiguration(NodeConfiguration):
# Blockchain
miner_agent: EthereumContractAgent = None,
checksum_address: str = None,
registry_filepath: str = None,
*args, **kwargs
) -> None:
@ -48,16 +45,15 @@ class UrsulaConfiguration(NodeConfiguration):
# REST
self.rest_host = rest_host or self.DEFAULT_REST_HOST
self.rest_port = rest_port or self. DEFAULT_REST_PORT
self.db_name = db_name or self.DEFAULT_DB_TEMPLATE.format(port=self.rest_port)
self.db_name = db_name or self.__DB_TEMPLATE.format(port=self.rest_port)
self.db_filepath = db_filepath or constants.UNINITIALIZED_CONFIGURATION
#
# TLS
#
self.tls_curve = tls_curve or self.DEFAULT_TLS_CURVE
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
self.tls_private_key = tls_private_key
self.certificate = certificate
self.certificate_filepath = certificate_filepath
# Ursula
self.interface_signature = interface_signature
@ -68,7 +64,6 @@ class UrsulaConfiguration(NodeConfiguration):
#
self.miner_agent = miner_agent
self.checksum_address = checksum_address
self.registry_filepath = registry_filepath or constants.UNINITIALIZED_CONFIGURATION
super().__init__(*args, **kwargs)
@ -80,18 +75,12 @@ class UrsulaConfiguration(NodeConfiguration):
instance = cls(**{**payload, **overrides})
return instance
def _generate_runtime_filepaths(self, commit=True) -> dict:
base_filepaths = super()._generate_runtime_filepaths(commit=commit)
# TODO: Handle pre-existing certificates, injecting the path
# if not self.certificate_filepath:
# certificate_filepath = certificate_filepath or os.path.join(self.known_certificates_dir, 'ursula.pem')
filepaths = dict(db_filepath=os.path.join(self.config_root, self.db_name),
registry_filepath=os.path.join(self.config_root, self.__REGISTRY_NAME))
if commit:
for field, filepath in filepaths.items():
setattr(self, field, filepath)
def generate_runtime_filepaths(self, config_root: str) -> dict:
base_filepaths = NodeConfiguration.generate_runtime_filepaths(config_root=config_root)
filepaths = dict(db_filepath=os.path.join(config_root, self.db_name),
)
base_filepaths.update(filepaths)
return filepaths
return base_filepaths
@property
def payload(self) -> dict:
@ -129,6 +118,10 @@ class UrsulaConfiguration(NodeConfiguration):
from nucypher.characters.lawful import Ursula
ursula = Ursula(**merged_parameters)
# if self.save_metadata: # TODO: Does this belong here..?
ursula.write_node_metadata(node=ursula)
ursula.save_certificate_to_disk(directory=ursula.known_certificates_dir) # TODO: Move this
if self.temp:
class MockDatastoreThreadPool(object): # TODO: Does this belong here..?
def callInThread(self, f, *args, **kwargs):
@ -137,18 +130,6 @@ class UrsulaConfiguration(NodeConfiguration):
return ursula
def load_known_nodes(self, known_metadata_dir=None) -> None:
if known_metadata_dir is None:
known_metadata_dir = self.known_metadata_dir
glob_pattern = os.path.join(known_metadata_dir, 'node-*.data')
metadata_paths = sorted(glob(glob_pattern), key=os.path.getctime)
for metadata_path in metadata_paths:
from nucypher.characters.lawful import Ursula
node = Ursula.from_metadata_file(filepath=abspath(metadata_path))
self.known_nodes.add(node)
class AliceConfiguration(NodeConfiguration):

View File

@ -1,8 +1,8 @@
import contextlib
import os
from abc import abstractmethod
from glob import glob
from os.path import abspath
from tempfile import TemporaryDirectory
from typing import Iterable
from constant_sorrow import constants
from itertools import islice
@ -14,17 +14,21 @@ from nucypher.network.middleware import RestMiddleware
class NodeConfiguration:
DEFAULT_OPERATING_MODE = 'decentralized'
TEMP_CONFIGURATION_DIR_PREFIX = "nucypher-tmp-cli-"
DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
__TEMP_CONFIGURATION_DIR_PREFIX = "nucypher-tmp-cli-"
__REGISTRY_NAME = 'contract_registry.json'
__DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
class ConfigurationError(RuntimeError):
pass
class InvalidConfiguration(RuntimeError):
pass
def __init__(self,
temp: bool = True,
temp: bool = False,
auto_initialize: bool = False,
config_root: str = None,
config_root: str = DEFAULT_CONFIG_ROOT,
config_file_location: str = DEFAULT_CONFIG_FILE_LOCATION,
keyring_dir: str = None,
@ -33,86 +37,74 @@ class NodeConfiguration:
is_me: bool = True,
federated_only: bool = None,
network_middleware: RestMiddleware = None,
registry_filepath: str = None,
# Informant
known_metadata_dir: str = None,
start_learning_on_same_thread: bool = False,
# Learner
learn_on_same_thread: bool = False,
abort_on_learning_error: bool = False,
always_be_learning: bool = True,
known_nodes: Iterable = None,
save_metadata: bool = True
start_learning_now: bool = True,
# Metadata
known_nodes: set = None,
known_metadata_dir: str = None,
load_metadata: bool = False,
save_metadata: bool = False
) -> None:
#
# Configuration root
# Configuration Filepaths
#
self.temp = temp
self.__temp_dir = constants.LIVE_CONFIGURATION
if temp and not config_root:
# Create a temp dir and set it as the config root if no config root was specified
self.__temp_dir = constants.UNINITIALIZED_CONFIGURATION
config_root = constants.UNINITIALIZED_CONFIGURATION
elif not config_root:
config_root = DEFAULT_CONFIG_ROOT
self.config_root = config_root
#
# Node Filepaths (Configuration root files and subdirectories)
#
self.config_file_location = config_file_location
self.keyring_dir = keyring_dir or constants.UNINITIALIZED_CONFIGURATION
self.known_nodes_dir = constants.UNINITIALIZED_CONFIGURATION
self.known_certificates_dir = known_metadata_dir or constants.UNINITIALIZED_CONFIGURATION
self.known_metadata_dir = known_metadata_dir or constants.UNINITIALIZED_CONFIGURATION
self.registry_filepath = registry_filepath or constants.UNINITIALIZED_CONFIGURATION
if auto_initialize:
self.initialize_configuration() # <<< Write runtime files and dirs
self.temp = temp
self.__temp_dir = constants.LIVE_CONFIGURATION
if temp:
# Create a temp dir and set it as the config root if no config root was specified
self.__temp_dir = constants.UNINITIALIZED_CONFIGURATION
config_root = constants.UNINITIALIZED_CONFIGURATION
else:
self.__cache_runtime_filepaths(config_root=config_root)
#
# Node
#
if not federated_only: # TODO: get_config function?
federated_only = True if self.DEFAULT_OPERATING_MODE is 'federated' else False
self.federated_only = federated_only
if is_me:
network_middleware = network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE_CLASS()
self.network_middleware = network_middleware
self.config_root = config_root
self.config_file_location = config_file_location
#
# Identity
#
self.is_me = is_me
self.checksum_address = checksum_address
if not federated_only: # TODO: get_config function?
federated_only = True if self.DEFAULT_OPERATING_MODE is 'federated' else False
self.federated_only = federated_only
# Learning
self.known_nodes = known_nodes
self.start_learning_on_same_thread = start_learning_on_same_thread
#
# Network & Learning
#
if is_me:
network_middleware = network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS()
self.network_middleware = network_middleware
self.known_nodes = known_nodes or set()
self.learn_on_same_thread = learn_on_same_thread
self.abort_on_learning_error = abort_on_learning_error
self.always_be_learning = always_be_learning
self.start_learning_now = start_learning_now
self.save_metadata = save_metadata
def _write_default_configuration_file(self, filepath: str = DEFAULT_CONFIG_FILE_LOCATION):
with contextlib.ExitStack() as stack:
template_file = stack.enter_context(open(TEMPLATE_CONFIG_FILE_LOCATION, 'r'))
new_file = stack.enter_context(open(filepath, 'w+'))
if new_file.read() != '':
raise self.ConfigurationError("{} is not a blank file. Do you have an existing configuration file?")
#
# Auto-Initialization
#
for line in islice(template_file, 12, None): # chop the warning header
new_file.writelines(line.lstrip(';')) # TODO Copy Default Sections, Perhaps interactively
def check_config_tree(self, configuration_dir: str = None) -> bool: # TODO: more filesystem validation
path = configuration_dir if configuration_dir else self.config_root
if not os.path.exists(path):
raise self.ConfigurationError(
'No configuration directory found at {}.'.format(configuration_dir))
return True
@property
def runtime_filepaths(self):
return self._generate_runtime_filepaths(commit=False)
if auto_initialize:
self.write_defaults() # <<< Write runtime files and dirs
if load_metadata:
self.load_known_nodes(known_metadata_dir=known_metadata_dir)
@property
def payload(self):
@ -125,48 +117,62 @@ class NodeConfiguration:
# keyring_dir=self.keyring_dir, # TODO: local private keys
# Behavior
start_learning_on_same_thread=self.start_learning_on_same_thread,
learn_on_same_thread=self.learn_on_same_thread,
abort_on_learning_error=self.abort_on_learning_error,
always_be_learning=self.always_be_learning,
start_learning_now=self.start_learning_now,
network_middleware=self.network_middleware,
# Knowledge
known_nodes=self.known_nodes,
known_certificates_dir=self.known_certificates_dir,
known_metadata_dir=self.known_metadata_dir
known_metadata_dir=self.known_metadata_dir,
save_metadata=self.save_metadata
)
return base_payload
def _generate_runtime_filepaths(self, commit=True) -> dict:
@staticmethod
def generate_runtime_filepaths(config_root: str) -> dict:
"""Dynamically generate paths based on configuration root directory"""
if self.temp and commit and self.config_root is constants.UNINITIALIZED_CONFIGURATION:
raise self.ConfigurationError("Cannot pre-generate filepaths for temporary node configurations.")
filepaths = dict(config_root=self.config_root,
keyring_dir=os.path.join(self.config_root, 'keyring'),
known_nodes_dir=os.path.join(self.config_root, 'known_nodes'),
known_certificates_dir=os.path.join(self.config_root, 'certificates'),
known_metadata_dir=os.path.join(self.config_root, 'metadata'))
if commit:
for field, filepath in filepaths.items():
setattr(self, field, filepath)
known_nodes_dir = os.path.join(config_root, 'known_nodes')
filepaths = dict(config_root=config_root,
keyring_dir=os.path.join(config_root, 'keyring'),
known_nodes_dir=known_nodes_dir,
known_certificates_dir=os.path.join(known_nodes_dir, 'certificates'),
known_metadata_dir=os.path.join(known_nodes_dir, 'metadata'),
registry_filepath=os.path.join(config_root, NodeConfiguration.__REGISTRY_NAME))
return filepaths
def cleanup(self):
if self.temp:
self.__temp_dir.cleanup()
@staticmethod
def check_config_tree_exists(config_root: str) -> bool:
# Top-level
if not os.path.exists(config_root):
raise NodeConfiguration.ConfigurationError('No configuration directory found at {}.'.format(config_root))
def initialize_configuration(self) -> str:
"""Create the configuration and runtime directory tree starting with thr config root directory."""
# Sub-paths
filepaths = NodeConfiguration.generate_runtime_filepaths(config_root=config_root)
for field, path in filepaths.items():
if not os.path.exists(path):
message = 'Missing configuration directory {}.'
raise NodeConfiguration.InvalidConfiguration(message.format(path))
return True
def __cache_runtime_filepaths(self, config_root: str) -> None:
"""Generate runtime filepaths and cache them on the config object"""
filepaths = self.generate_runtime_filepaths(config_root=config_root)
for field, filepath in filepaths.items():
setattr(self, field, filepath)
def write_defaults(self) -> str:
"""Writes the configuration and runtime directory tree starting with the config root directory."""
#
# Create Config Root
#
if self.temp and self.config_root is constants.UNINITIALIZED_CONFIGURATION:
self.__temp_dir = TemporaryDirectory(prefix=self.TEMP_CONFIGURATION_DIR_PREFIX)
if self.temp:
self.__temp_dir = TemporaryDirectory(prefix=self.__TEMP_CONFIGURATION_DIR_PREFIX)
self.config_root = self.__temp_dir.name
if not self.temp and self.config_root is not constants.UNINITIALIZED_CONFIGURATION:
else:
try:
os.mkdir(self.config_root, mode=0o755)
except FileExistsError:
@ -179,13 +185,19 @@ class NodeConfiguration:
#
# Create Config Subdirectories
#
self._generate_runtime_filepaths(commit=True)
self.__cache_runtime_filepaths(config_root=self.config_root)
try:
# Directories
os.mkdir(self.keyring_dir, mode=0o700) # keyring
os.mkdir(self.known_nodes_dir, mode=0o755) # known_nodes
os.mkdir(self.known_certificates_dir, mode=0o755) # known_certs
os.mkdir(self.known_metadata_dir, mode=0o755) # known_metadata
# Files
with open(self.registry_filepath, 'w') as registry_file:
registry_file.write('MOCK REGISTRY') # TODO: write the default registry
except FileExistsError:
# TODO: beef up the error message
# existing_paths = [os.path.join(self.config_root, f) for f in os.listdir(self.config_root)]
@ -193,15 +205,37 @@ class NodeConfiguration:
message = "There are pre-existing nucypher installation files at {}".format(self.config_root)
raise NodeConfiguration.ConfigurationError(message)
self.check_config_tree(configuration_dir=self.config_root)
self.check_config_tree_exists(config_root=self.config_root)
return self.config_root
@classmethod
@abstractmethod
def from_configuration_file(cls, filepath: str):
raise NotImplementedError
def load_known_nodes(self, known_metadata_dir=None) -> None:
if known_metadata_dir is None:
known_metadata_dir = self.known_metadata_dir
glob_pattern = os.path.join(known_metadata_dir, '*.node')
metadata_paths = sorted(glob(glob_pattern), key=os.path.getctime)
for metadata_path in metadata_paths:
from nucypher.characters.lawful import Ursula
node = Ursula.from_metadata_file(filepath=abspath(metadata_path), federated_only=self.federated_only)
self.known_nodes.add(node)
def write_default_configuration_file(self, filepath: str = DEFAULT_CONFIG_FILE_LOCATION):
with contextlib.ExitStack() as stack:
template_file = stack.enter_context(open(TEMPLATE_CONFIG_FILE_LOCATION, 'r'))
new_file = stack.enter_context(open(filepath, 'w+'))
if new_file.read() != '':
message = "{} is not a blank file. Do you have an existing configuration file?"
raise self.ConfigurationError(message)
for line in islice(template_file, 12, None): # chop the warning header
new_file.writelines(line.lstrip(';')) # TODO Copy Default Sections, Perhaps interactively
def cleanup(self):
if self.temp:
self.__temp_dir.cleanup()
@classmethod
@abstractmethod
def from_configuration_directory(cls, filepath: str):
def from_configuration_file(cls, filepath: str):
raise NotImplementedError

View File

@ -61,9 +61,9 @@ def parse_character_config(config=None, filepath: str=DEFAULT_CONFIG_FILE_LOCATI
federated_only = False
character_payload = dict(federated_only=federated_only,
start_learning_on_same_thread=config.getboolean(section='character', option='start_learning_on_same_thread'),
start_learning_on_same_thread=config.getboolean(section='character', option='learn_on_same_thread'),
abort_on_learning_error=config.getboolean(section='character', option='abort_on_learning_error'),
always_be_learning=config.getboolean(section='character', option='always_be_learning'))
always_be_learning=config.getboolean(section='character', option='start_learning_now'))
return character_payload

View File

@ -40,7 +40,7 @@ def validate_configuration_file(config=None,
if not config.sections():
raise NodeConfiguration.ConfigurationError("Empty configuration file")
raise NodeConfiguration.InvalidConfiguration("Empty configuration file")
required_sections = ("nucypher", "blockchain")

View File

@ -57,7 +57,7 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
"""
from nucypher.characters.lawful import Bob
bob = Bob(network_middleware=MockRestMiddleware(),
always_be_learning=False,
start_learning_now=False,
abort_on_learning_error=True,
federated_only=True)

View File

@ -19,7 +19,7 @@ def test_actor_without_signing_power_cannot_sign():
"""
cannot_sign = CryptoPower(power_ups=[])
non_signer = Character(crypto_power=cannot_sign,
always_be_learning=False,
start_learning_now=False,
federated_only=True)
# The non-signer's stamp doesn't work for signing...
@ -40,7 +40,7 @@ def test_actor_with_signing_power_can_sign():
message = b"Llamas."
signer = Character(crypto_power_ups=[SigningPower], is_me=True,
always_be_learning=False, federated_only=True)
start_learning_now=False, federated_only=True)
stamp_of_the_signer = signer.stamp
# We can use the signer's stamp to sign a message (since the signer is_me)...
@ -61,10 +61,10 @@ def test_anybody_can_verify():
Here, we show that anybody can do it without needing to directly access Crypto.
"""
# Alice can sign by default, by dint of her _default_crypto_powerups.
alice = Alice(federated_only=True, always_be_learning=False)
alice = Alice(federated_only=True, start_learning_now=False)
# So, our story is fairly simple: an everyman meets Alice.
somebody = Character(always_be_learning=False, federated_only=True)
somebody = Character(start_learning_now=False, federated_only=True)
# Alice signs a message.
message = b"A message for all my friends who can only verify and not sign."
@ -126,7 +126,7 @@ def test_anybody_can_encrypt():
"""
Similar to anybody_can_verify() above; we show that anybody can encrypt.
"""
someone = Character(always_be_learning=False, federated_only=True)
someone = Character(start_learning_now=False, federated_only=True)
bob = Bob(is_me=False, federated_only=True)
cleartext = b"This is Officer Rod Farva. Come in, Ursula! Come in Ursula!"