Source code for paradrop.base.nexus

'''
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

import smokesignal
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue

from . import output, settings
from paradrop.lib.utils import pdosq

# 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 = pdosq.read_yaml_file(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 = pdosq.read_yaml_file(path, default={}) 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.safe_dump(contents, default_flow_style=False))