'''
Stateful, singleton, paradrop daemon command center.
See docstring for NexusBase class for information on settings.
SETTINGS QUICK REFERENCE:
# assuming the following import
from paradrop.base import nexus
nexus.core.info.version
nexus.core.info.pdid
'''
import os
import yaml
import json
from enum import Enum
import smokesignal
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue
from . import output, settings
# Global access. Assign this wherever you instantiate the Nexus object:
# nexus.core = MyNexusSubclass()
core = None
[docs]class NexusBase(object):
'''
Resolving these values to their final forms:
1 - module imported, initial values assigned(as written below)
2 - class is instatiated, passed settings to replace values
3 - instance chooses appropriate values based on current state(production or local)
Each category has its own method for initialization here
(see: resolveNetwork, resolvePaths)
'''
VERSION = 1 # nexus.core.info.version
PDID = None # nexus.core.info.pdid
def __init__(self, stealStdio=True, printToConsole=True):
'''
The one big thing this function leaves out is reactor.start(). Call this externally
*after * initializing a nexus object.
'''
self.session = None
self.wamp_connected = False
self.jwt_valid = False
self.info = AttrWrapper()
resolveInfo(self, settings.CONFIG_FILE)
self.info.setOnChange(self.onInfoChange)
# initialize output. If filepath is set, logs to file.
# If stealStdio is set intercepts all stderr and stdout and interprets it internally
# If printToConsole is set (defaults True) all final output is rendered to stdout
output.out.startLogging(filePath=settings.LOG_DIR, stealStdio=stealStdio, printToConsole=printToConsole)
# register onStop for the shutdown call
reactor.addSystemEventTrigger('before', 'shutdown', self.onStop)
# The reactor needs to be runnnig before this call is fired, since we start the session
# here. Assuming callLater doesn't fire until thats happened
reactor.callLater(0, self.onStart)
[docs] def onStart(self):
pdid = self.info.pdid if self.provisioned() else 'UNPROVISIONED'
output.out.usage('%s coming up' % (pdid))
[docs] def onStop(self):
self.save()
output.out.usage('%s going down' % (self.info.pdid))
smokesignal.clear_all()
output.out.endLogging()
[docs] @inlineCallbacks
def connect(self, sessionClass, debug=False):
'''
Takes the given session class and attempts to connect to the crossbar fabric.
If an existing session is connected, it is cleanly closed.
'''
if (self.session is not None):
yield self.session.leave()
self.wamp_connected = False
output.out.info('Connecting to wamp router at URI: %s' % str(self.info.wampRouter))
# Setting self.session here only works for the first connection but
# becomes stale if the connection fails and we reconnect.
# In that case a new session object is automatically created.
# For this reason, we also update this session reference in BaseSession.onJoin.
self.session = yield sessionClass.start(self.info.wampRouter, self.info.pdid, debug=debug)
returnValue(self.session)
[docs] def onInfoChange(self, key, value):
'''
Called when an internal setting is changed. Trigger a save automatically.
'''
self.save()
[docs] def save(self):
''' Ehh. Ideally this should happen asynchronously. '''
saveDict = self.info.__dict__['contents']
saveDict['version'] = self.info.version
writeYaml(saveDict, settings.CONFIG_FILE)
#########################################################
# High Level Methods
#########################################################
[docs] def provisioned(self):
'''
Checks if this[whatever] appears to be provisioned or not
'''
return self.info.pdid is not None
[docs] def provision(self, pdid, pdserver=settings.PDSERVER, wampRouter=settings.WAMP_ROUTER):
self.info.pdid = pdid
self.info.pdserver = pdserver
self.info.wampRouter = wampRouter
#########################################################
# Keys
#########################################################
[docs] def saveKey(self, key, name):
''' Save the key with the given name. Overwrites by default '''
path = settings.KEY_DIR + name
with open(path, 'wb') as f:
f.write(key)
[docs] def getKey(self, name):
''' Returns the given key or None '''
path = settings.KEY_DIR + name
if os.path.isfile(path):
with open(path, 'rb') as f:
return f.read()
return None
#########################################################
# Misc
#########################################################
def __repr__(self):
''' Dump everything '''
dic = dict(info=self.info.contents)
return json.dumps(dic, sort_keys=True, indent=4)
#########################################################
# Utils
#########################################################
[docs]class AttrWrapper(object):
'''
Simple attr interceptor to make accessing settings simple.
Stores values in an internal dict called contents.
Does not allow modification once _lock() is called. Respect it.
Once you've filled it up with the appropriate initial values, set
onChange to assign
'''
def __init__(self):
self.__dict__['contents'] = {}
# Called when a value changes unless None
self.__dict__['onChange'] = None
# Lock the contents of this wrapper. Can read valued, cant write them
self.__dict__['locked'] = False
def _lock(self):
self.__dict__['locked'] = True
[docs] def setOnChange(self, func):
assert(callable(func))
self.__dict__['onChange'] = func
def __repr__(self):
return str(self.contents)
def __getattr__(self, name):
return self.__dict__['contents'][name]
def __setattr__(self, k, v):
if self.__dict__['locked']:
raise AttributeError('This attribute wrapper is locked. You cannot change its values.')
self.contents[k] = v
if self.__dict__['onChange'] is not None:
self.__dict__['onChange'](k, v)
[docs]def resolveInfo(nexus, path):
'''
Given a path to the config file, load its contents and assign it to the
config file as appropriate.
'''
# Check to make sure we have a default settings file
if not os.path.isfile(path):
createDefaultInfo(path)
contents = loadYaml(path)
# Sanity check contents of info and throw it out if bad
if not validateInfo(contents):
output.out.err('Saved configuration data invalid, destroying it.')
os.remove(path)
createDefaultInfo(path)
contents = loadYaml(path)
writeYaml(contents, path)
nexus.info.pdid = contents['pdid']
nexus.info.version = contents['version']
nexus.info.pdserver = contents['pdserver']
nexus.info.wampRouter = contents['wampRouter']
[docs]def createDefaultInfo(path):
default = {
'version': 1,
'pdid': None,
'pdserver': settings.PDSERVER,
'wampRouter': settings.WAMP_ROUTER
}
writeYaml(default, path)
[docs]def validateInfo(contents):
'''
Error checking on the read YAML file. This is a temporary method.
:
param contents:
the read - in yaml to check
:
type contents:
dict.
:
returns:
True if valid, else false
'''
INFO_REQUIRES = ['version', 'pdid', 'pdserver', 'wampRouter']
for k in INFO_REQUIRES:
if k not in contents:
output.out.err('Contents is missing: ' + str(k))
return False
return True
[docs]def writeYaml(contents, path):
''' Overwrites content with YAML representation at given path '''
# print 'Writing ' + str(contents) + ' to path ' + str(path)
with open(path, 'w') as f:
f.write(yaml.dump(contents, default_flow_style=False))
[docs]def loadYaml(path):
''' Return dict from YAML found at path '''
with open(path, 'r') as f:
contents = yaml.load(f.read())
# print 'Loaded ' + str(contents) + ' from path ' + str(path)
return contents