class ChatRoom(Entity): """models the actions needed to interact in a chatroom""" implements(IDroneModelChatRoom) jabber = property(lambda s: services.getService('jabber')) jabber_config = property(lambda s: s.jabber.SERVICECONFIG) room = property(lambda s: s._room) jid = property(lambda s: '%s@%s' % \ (s.room, s.jabber_config.JABBER_CHAT_SERVICE)) nick = property(lambda s: s.jabber_config.JABBER_CHAT_NICK) def __init__(self, room): self._room = room self.conversation = Conversation(self.jid) self.conversation.groupChat = True def join(self): """join the chat room""" self.jabber.joinChatRoom(self.room) def leave(self): """leave the chat room""" self.jabber.leaveChatRoom(self.jid) def hear(self, message): """Only pay attention to messages that start with my chat nick""" if message.lower().split()[0] == self.nick: message = message[len(self.nick):].strip() self.conversation.hear(message)
class AdaptToNullProcess(object): """I can adapt a L{IDroneModelAppProcess} provider or a L{IDroneModelAppInstance} provider to a L{IKittNullProcess} provider. I hold no references to the Original Object after Instantiation. """ implements(IKittNullProcess) def __init__(self, original): self.process = NullProcess() #make an attempt to update the original caller if hasattr(original, 'pid'): try: original.pid = self.process.pid except: pass if hasattr(original, 'inode'): try: original.inode = self.process.inode except: pass def __getattribute__(self, name): try: return object.__getattribute__(self, name) except: return self.process.__getattribute__(name)
class ProcessSnapshot(Process): """Snapshot of process information""" implements(IKittProcessSnapshot) def __init__(self, *args): Process.__init__(self, *args) self.update() def update(self): """Update the snapshot""" stats = os.stat(self.path) self.uid = stats.st_uid self.gid = stats.st_gid self.cmdline = self.readFile('cmdline').split('\000')[:-1] try: self.cwd = os.readlink('%s/cwd' % self.path) except: self.cwd = None # self.environ = self.getEnv() try: self.exe = os.readlink('%s/exe' % self.path) except: self.exe = None self.fd = self.getFD() self.tasks = self.getTasks() try: self.root = os.readlink('%s/root' % self.path) except: self.root = None self.tgid = int( self.readFile('status').split('Tgid:', 1)[1].split(None, 1)[0]) vars(self).update(self.getStats())
class AdaptToProcess(object): """I can adapt a L{IDroneModelAppProcess} provider or a L{IDroneModelAppInstance} provider to a L{IKittProcess} provider. I hold no references to the Original Object after Instantiation. """ implements(IKittProcess) def __init__(self, original): self.process = NullProcess() #assume the process is dead if original.server.hostname == config.HOSTNAME: #delay scanning the process, for as long as possible try: self.process = ProcessSnapshot(original.pid) except InvalidProcess: pass #raise all others else: self.process = RemoteProcess(original.pid) #make an attempt to update the original caller if hasattr(original, 'pid'): try: original.pid = self.process.pid except: pass if hasattr(original, 'inode'): try: original.inode = self.process.inode except: pass def __getattribute__(self, name): try: return object.__getattribute__(self, name) except: return self.process.__getattribute__(name)
class RemoteProcess(NullProcess): """This is a remote process that looks like a live process""" implements(IKittRemoteProcess) pid = property(lambda s: s.info.get('pid', 0)) inode = property(lambda s: s.info.get('inode', 0)) ppid = property(lambda s: s.info.get('ppid', 0)) memory = property(lambda s: s.info.get('memory', 0)) fd_count = property(lambda s: s.info.get('fd_count', 0)) stats = property(lambda s: s.info.get('stats', {})) threads = property(lambda s: s.info.get('threads', 0)) exe = property(lambda s: s.info.get('exe', None)) cmdline = property(lambda s: s.info.get('cmdline', [])) environ = property(lambda s: s.info.get('environ', {})) def __init__(self, pid): self.info = {'pid': pid} def updateProcess(self, infoDict): self.info.update(infoDict) def isRunning(self): return bool(self.pid) def memUsage(self): return self.memory def getFD(self): return [ i for i in self.fd_count ] def getStats(self): return self.stats def getEnv(self): return self.environ def getTasks(self): return set([ i for i in self.threads ]) def cpuUsage(self): return { 'user_util': self.info.get('user_util', 0.0), 'sys_util': self.info.get('sys_util', 0.0), } def __str__(self): return '%s(pid=%d)' % (self.__class__.__name__, self.pid) __repr__ = __str__
class Process(object): """Represents a process A platform specific backend should be developed The purpose of this class is to demonstrate a skeleton for the implementation of IKittProcess. """ implements(IKittProcess) #expected attributes running = property(lambda s: s.isRunning()) inode = property(lambda s: 0) #figure out how to get pid = property(lambda s: 0) #figure out how to get ppid = property(lambda s: 0) #figure out how to get exe = property(lambda s: None) #figure out how to get cmdline = property(lambda s: []) #figure out how to get memory = property(lambda s: s.memUsage()) fd_count = property(lambda s: len(s.getFD()) or 3) stats = property(lambda s: s.getStats()) environ = property(lambda s: s.getEnv()) threads = property(lambda s: len(s.getTasks())) def __init__(self, pid): raise NotImplemented() def isRunning(self): return False def getEnv(self): return {} def getFD(self): return {} def getTasks(self): return {} def getStats(self): return {} def memUsage(self): return 0 def cpuUsage(self): return { 'user_util': 0.0, 'sys_util': 0.0, } def __str__(self): return '%s(pid=%d)' % (self.__class__.__name__, self.pid) __repr__ = __str__
class ProcessSnapshot(Process): """Snapshot of process information""" implements(IKittProcessSnapshot) def __init__(self, *args): Process.__init__(self, *args) self.update() def update(self): """Update the snapshot""" vars(self).update(self.getStats()) self.cwd = os.readlink('%s/path/cwd' % self.path) self.environ = self.getEnv() self.exe = self.fname[0] self.fd = self.getFD() self.tasks = self.getTasks() self.root = os.readlink('%s/path/root' % self.path)
class NullProcess(Process): """Represents a non-existant process""" # Note: you should not need to override # this implemenation, but you can if you # want too implements(IKittNullProcess) pid = property(lambda s: 0) ppid = property(lambda s: 0) inode = property(lambda s: 0) exe = property(lambda s: None) cmdline = property(lambda s: []) fd_count = property(lambda s: 0) def __init__(self, pid=0): self._pid = 0 def isRunning(self): return False def getEnv(self): return {} def getFD(self): return {} def getStats(self): return {} def getTasks(self): return {} def memUsage(self): return 0 def cpuUsage(self): return { 'user_util': 0.0, 'sys_util': 0.0, }
class RPMApplication(ApplicationPlugin): """I am a standard RPM Application Plugin. I should work for standard rpm'd installations given you romeo config shortname is the same as the name of your rpm. """ SEARCH_DELAY = 5.0 #override in romeo implements(IDroneDApplication) def __init__(self, *args, **kwargs): Event('instance-found').subscribe(self._check_version) @safe(None) def _check_version(self, occurrence): ai = occurrence.instance if ai.app.name == self.name: return self._updateVersion(None, ai.label) def _updateVersion(self, result, label): thisInst = self.getInstance(label) try: ts = rpm.TransactionSet() mi = ts.dbMatch('name', self.name) for h in mi: if not h: break version = AppVersion.makeVersion(self.name, h['version']) if version > thisInst.appversion: thisInst.version = version except: err('exception while setting version') return result def startInstance(self, label): """I query the RPM DB and make sure the version is up to date""" d = ApplicationPlugin.startInstance(self, label) d.addCallback(self._updateVersion, label) return d #return the deferable object
class Action(Entity): implements(IDroneModelAction) completed = property(lambda self: self.deferred.called) succeeded = property(lambda self: self.completed and not \ isinstance(self.outcome,Failure) ) failed = property(lambda self: self.completed and not self.succeeded) stale = property(lambda self: self.finishedAt and \ time.time() - self.finishedAt > config.ACTION_EXPIRATION_TIME) finishedAt = None outcome = None def __init__(self, description, deferred): self.description = description self.deferred = deferred self.startedAt = time.time() self.context = {} deferred.addBoth(self.__finish) def __finish(self, outcome): self.outcome = outcome self.finishedAt = time.time() config.reactor.callLater(config.ACTION_EXPIRATION_TIME, Action.delete, self) return outcome
class ConversationContext(EntityContext): implements(IConversationContext) entityAttr = 'conversation' specialKeys = ['conversation', 'buddy', 'agent', 'issue', 'sop'] def get_conversation(self): return self.conversation def get_buddy(self): return self.conversation.buddy def get_agent(self): if SupportAgent.exists(self.conversation.buddy): return SupportAgent(self.conversation.buddy) def get_issue(self): agent = self['agent'] if agent: return agent.currentIssue def get_sop(self): agent = self['agent'] if agent: return agent.sop
class ProcessSnapshot(object): """Snapshot of process information""" implements(IKittProcessSnapshot) info = property(lambda s: s._data) pid = property(lambda s: s._pid) inode = property(lambda s: s.info.get('inode', 0)) ppid = property(lambda s: s.info.get('ppid', 0)) memory = property(lambda s: s.memUsage()) fd_count = property(lambda s: len(s.getFD()) or 3) stats = property(lambda s: s.getStats()) threads = property(lambda s: s.info.get('threads', 0)) exe = property(lambda s: s.info.get('exe', None)) environ = property(lambda s: s.getEnv()) cmdline = property(lambda s: s.info.get('cmdline', [])) running = property(lambda s: s.isRunning()) uid = property(lambda s: s.info.get('uid', 0)) gid = property(lambda s: s.info.get('gid', 0)) def __del__(self): if self._task.running: self._task.stop() def __init__(self, pid): self.deferred = defer.succeed(None) self._lastupdate = 0 self._pid = pid self._data = {} self._deferred = defer.succeed(None) self._task = task.LoopingCall(self._poll) try: LiveProcess(pid) #warm the cache self._task.start(process_poll) except InvalidProcess: self.__class__.delete(self) def _poll(self): if self.deferred.called: self.deferred = self.update() self.deferred.addErrback(lambda x: None) def isRunning(self): try: return LiveProcess(self.pid).running except: #destroy stats on death self.__class__.delete(self) self._data = {} return False @synchronizedDeferred(semaphore) @defer.deferredGenerator def update(self): result = {} try: if (time.time() - self._lastupdate) < process_poll * 2: result = self._data #throttle scans else: self._lastupdate = time.time() self.deferred = self._get_updates() wfd = defer.waitForDeferred(self.deferred) yield wfd self._data.update(wfd.getResult()) result = self._data except: if self._task.running: self._task.stop() self.__class__.delete(self) self._data = {} #destroy stats on death result = Failure() yield result def getTasks(self): return self.info.get('getTasks', set()) def getStats(self): return self.info.get('getStats', {}) def getFD(self): return self.info.get('getFD', {}) def getEnv(self): return self.info.get('getEnv', {}) def memUsage(self): return self.info.get('memUsage', 0) def cpuUsage(self): return self.info.get('cpuUsage', {'user_util': 0.0, 'sys_util': 0.0}) @deferredAsThread def _get_updates(self): """lots of io in here on most platforms""" #each look will keep the process honest return { 'getTasks': LiveProcess(self.pid).getTasks(), 'getStats': LiveProcess(self.pid).getStats(), 'memUsage': LiveProcess(self.pid).memUsage(), 'cpuUsage': LiveProcess(self.pid).cpuUsage(), 'getFD': LiveProcess(self.pid).getFD(), 'getEnv': LiveProcess(self.pid).getEnv(), 'ppid': LiveProcess(self.pid).ppid, 'cmdline': LiveProcess(self.pid).cmdline, 'exe': LiveProcess(self.pid).exe, 'uid': LiveProcess(self.pid).uid, 'gid': LiveProcess(self.pid).gid, 'inode': LiveProcess(self.pid).inode, 'threads': LiveProcess(self.pid).threads } def __str__(self): return '%s(pid=%d)' % (self.__class__.__name__, self.pid) __repr__ = __str__
class Server(Entity): implements(IDroneModelServer) connectFailure = None appinstances = property( lambda self: (i for i in AppInstance.objects if \ i.server is self) ) apps = property(lambda self: (a for a in App.objects if self in a.shouldRunOn)) scabs = property(lambda self: (s for s in Scab.objects if s.server is self)) unreachable = property(lambda self: self.connectFailure is not None) #FIXME this doesn't seem right we should remove it installedApps = property(lambda self: set(av.app for av in self.installed)) debug = False serializable = True listed = False logs = {} def __init__(self, hostname): self.hostname = hostname self.installed = {} self.droned = DroneD(self) self.manager = ServerManager(self) def __getstate__(self): installed = {} for appversion, configs in self.installed.items(): av = (appversion.app.name, appversion.version) installed[av] = [] for configpkg in configs: cp = (configpkg.name, configpkg.version) installed[av].append(cp) return { 'hostname': self.hostname, 'connectFailure': self.connectFailure, 'debug': self.debug, 'installed': installed, } @staticmethod def construct(state): server = Server(state['hostname']) if state['connectFailure'] != server.connectFailure: server.connectFailure = state['connectFailure'] if state['debug'] != server.debug: server.debug = state['debug'] if 'installed' in state: for av, configs in state['installed'].items(): app, version = App(av[0]), av[1] av = AppVersion(app, version) server.installed[av] = set( ConfigPackage(*cp) for cp in configs) return server @staticmethod def byName(name): if Server.exists(name): return Server(name) for server in Server.objects: if server.hostname.startswith(name): return server def startPolling(self): self.droned.startPolling() def stopPolling(self): self.droned.stopPolling()
class ProcessSnapshot(Process): """Snapshot of process information""" implements(IKittProcessSnapshot) pass
class AppVersion(Entity): """Track application versions""" implements(IDroneModelAppVersion) description = property( lambda self: "%s %s" % \ (self.app.name, self.version_string) ) serializable = True def __getattribute__(self, name): """Overrode to fulfill Interface Obligations""" if name in ('package','major','minor','micro','prerelease','base',\ 'short'): return self.version.__getattribute__(name) return object.__getattribute__(self, name) def __init__(self, *args, **kwargs): self.version = Version(*args, **kwargs) self.app = IDroneModelApp(self.version) @property def version_string(self): """makes a nice version string that we can use for reconstruction""" result = '.'.join([ str(self.major), str(self.minor), str(self.micro), ]) if self.prerelease: result += '.%s' % str(self.prerelease) return result def __getstate__(self): return { 'app': self.package, 'version': self.version_string, #for proper serialization } def __cmp__(self, other): """overrode for easy comparison of AppVersion to AppVersion and Version to AppVersion. @raise IncomparableVersions: when the package names of the versions differ. @param other L{twisted.python.versions.Version}, L{IDroneModelAppVersion}, or object @return C{int} of value -1, 0, or 1 """ try: if IDroneModelAppVersion.providedBy(other): return self.version.__cmp__(other.version) except IncomparableVersions: raise except: pass if isinstance(other, Version): return self.version.__cmp__(other) return object.__cmp__(self, other) @staticmethod def makeAppVersion(name, version): """Similar to ``makeArgs`` @return L{IDroneModelAppVersion} provider """ args, kwargs = AppVersion.makeArgs(name, version) return AppVersion(*args, **kwargs) @staticmethod def makeVersion(name, version): """Similar to ``makeArgs`` @return L{twisted.python.versions.Version} """ args, kwargs = AppVersion.makeArgs(name, version) return Version(*args, **kwargs) @staticmethod def versionExists(name, version): """check if this L{IDroneModelAppVersion} provider exists""" args, kwargs = AppVersion.makeArgs(name, version) return AppVersion.exists(*args, **kwargs) @staticmethod def makeArgs(name, version): """goes through great lengths to make args for use by a constructor for class types - L{IDroneModelAppVersion} providers or L{twisted.python.versions.Version}. @raises TypeError - if version is not convertable @param name C{str} @param version C{str}, C{list}, C{tuple}, or L{twisted.python.versions.Version} @return ((name, major, minor, micro), {'prerelease': prerelease}) """ default = [0, 0, 0, 0] #last number denotes pre-release if isinstance(version, Version): if version.package == name: return ((name, version.major, version.minor, version.micro), { 'prerelease': version.prerelease }) else: version = [] if isinstance(version, str): version = version.split('.') elif isinstance(version, tuple): version = list(version) elif isinstance(version, type(None)): version = [] else: raise TypeError('Unacceptable version input %s' % type(version)) version = version + default #pad the length for comparisons v = [i[0] for i in zip(version, default)] kwargs = {'prerelease': None} try: #don't try too hard to get this right if len(v) == 4 and v[3]: kwargs['prerelease'] = int(v[3]) except: pass return (tuple((name, ) + tuple([int(i) for i in v[0:3]])), kwargs) @staticmethod def construct(state): name = state['app'] appversion = AppVersion.makeAppVersion(name, state['version']) return appversion
class JabberClient(object): implements(IDroneDService) #requirement xmlstream = None connected = property(lambda self: self.xmlstream is not None) parentService = None #interface required attribute service = None #interface required attribute SERVICENAME = 'jabber' #interface required attribute #this should be overrode in romeo SERVICECONFIG = dictwrapper({ 'JABBER_CHAT_NICK': config.HOSTNAME, 'JABBER_USERNAME': '******', 'JABBER_PASSWORD': '******', 'JABBER_SERVER': 'jabber.example.net', 'JABBER_CHAT_SERVICE': 'conference.jabber.example.net', 'JABBER_PORT': 5222, 'JABBER_RESOURCE': config.HOSTNAME, 'JABBER_BROADCAST_INTERVAL': 300, 'JABBER_VALIDATE_XML': True, 'JABBER_TRUST_ROOM': False, 'DEPUTY': '*****@*****.**', 'CONVERSATION_RESPONSE_PERIOD': 180, 'JABBER_JOIN_CHATROOM': False, 'JABBER_TEAM_ROSTER': os.path.join(config.DRONED_HOMEDIR, 'teams'), }) #interface required attribute def running(self): """interface requirement""" return bool(self.service) and self.service.running def install(self, _parentService): """interface requirement""" self.parentService = _parentService user = decrypt(self.SERVICECONFIG.JABBER_USERNAME) server = self.SERVICECONFIG.JABBER_SERVER resource = self.SERVICECONFIG.JABBER_RESOURCE self.jid = JID("%(user)s@%(server)s/%(resource)s" % locals()) self.broadcastTask = LoopingCall(self.broadcastPresence) self.sendQueue = [] self.authenticated = False #load all jabber responders, after configuration import droned.responders droned.responders.loadAll() def start(self): """interface requirement""" if self.running(): return self.factory = XMPPClientFactory( self.jid, decrypt(self.SERVICECONFIG.JABBER_PASSWORD)) self.factory.addBootstrap(STREAM_CONNECTED_EVENT, self.connectionMade) self.factory.addBootstrap(STREAM_END_EVENT, self.connectionLost) self.factory.addBootstrap(STREAM_AUTHD_EVENT, self.connectionAuthenticated) self.factory.addBootstrap(STREAM_ERROR_EVENT, self.receivedError) self.factory.addBootstrap(INIT_FAILED_EVENT, self.initFailed) self.service = TCPClient(self.SERVICECONFIG.JABBER_SERVER, self.SERVICECONFIG.JABBER_PORT, self.factory) self.service.setServiceParent(self.parentService) #build/rebuild jabber teams if not os.path.exists(self.SERVICECONFIG.JABBER_TEAM_ROSTER): try: os.makedirs(self.SERVICECONFIG.JABBER_TEAM_ROSTER) except: log('Cannot load team rosters because %s does not exits' % \ self.SERVICECONFIG.JABBER_TEAM_ROSTER) return for name in os.listdir(self.SERVICECONFIG.JABBER_TEAM_ROSTER): f = (self.SERVICECONFIG.JABBER_TEAM_ROSTER, name) if os.path.isfile('%s/%s' % f): Team(name) #preload team rosters def stop(self): """interface requirement""" if self.service: self.factory.stopTrying() self.factory.stopFactory() self.service.disownServiceParent() self.service.stopService() self.service = None def connectionMade(self, xmlstream): log('connection made') self.xmlstream = xmlstream def connectionLost(self, xmlstream): log('connection lost') self.authenticated = False if self.broadcastTask.running: self.broadcastTask.stop() if self.connected: Event('jabber-offline').fire() self.xmlstream = None def connectionAuthenticated(self, xmlstream): log('connection authenticated') self.authenticated = True if not self.broadcastTask.running: self.broadcastTask.start( self.SERVICECONFIG.JABBER_BROADCAST_INTERVAL) xmlstream.addObserver('/message', self.receivedMessage) xmlstream.addObserver('/presence', self.receivedPresence) xmlstream.addObserver('/iq', self.receivedIQ) xmlstream.addObserver('/error', self.receivedError) Event('jabber-online').fire() while self.sendQueue: self.xmlstream.send(self.sendQueue.pop(0)) def broadcastPresence(self): presence = Element(('jabber:client', 'presence')) #log('sending presence broadcast') self.xmlstream.send(presence) def sendMessage(self, to, body, useHTML=True, groupChat=False): message = Element(('jabber:client', 'message')) message['to'] = to message['type'] = (groupChat and 'groupchat') or 'chat' message.addElement('body', None, body) if useHTML: html = message.addElement('html', 'http://jabber.org/protocol/xhtml-im') htmlBody = html.addElement('body', 'http://www.w3.org/1999/xhtml') htmlBody.addRawXml(unicode(body)) if self.SERVICECONFIG.JABBER_VALIDATE_XML: validateXml(html.toXml()) #safeXml = filter(lambda char: ord(char) < 128, message.toXml()) #log('sending message: %s' % safeXml) log('sending message to %s: %s' % (to, body)) if self.authenticated: self.xmlstream.send(message) else: log("not connected, queueing message", warning=True) self.sendQueue.append(message) def requestAuthorization(self, to): request = Element((None, 'iq')) request['type'] = 'set' request['id'] = 'auth-request:%s' % to query = Element((None, 'query')) query['xmlns'] = 'jabber:iq:roster' item = Element((None, 'item')) item['jid'] = to item['name'] = to.split('@')[0] query.addChild(item) request.addChild(query) log('sending auth request: %s' % request.toXml()) self.xmlstream.send(request) def joinChatRoom(self, room): presence = Element((None, 'presence')) presence['from'] = self.jid.userhost() jid = '%s@%s/%s' % (room, self.SERVICECONFIG.JABBER_CHAT_SERVICE, self.SERVICECONFIG.JABBER_CHAT_NICK) presence['to'] = jid x = Element(('http://jabber.org/protocol/muc', 'x')) history = Element((None, 'history')) history['maxchars'] = '0' x.addChild(history) presence.addChild(x) log('sending join: %s' % presence.toXml()) self.xmlstream.send(presence) def leaveChatRoom(self, jid): if '/' not in jid: jid += '/' + self.SERVICECONFIG.JABBER_CHAT_NICK presence = Element((None, 'presence')) presence['from'] = self.jid.userhost() presence['to'] = jid presence['type'] = 'unavailable' log('sending leave: %s' % presence.toXml()) self.xmlstream.send(presence) def receivedMessage(self, e): # Extract the body of the message try: message = str([c for c in e.children if c.name == 'body'][0]) except: log('discarding invalid message (has no body!): %s' % e.toXml()) return # Discard delayed messages delays = [x for x in e.children if x.name == 'delay'] stamps = [ x for x in e.children \ if x.name == 'x' and \ x.compareAttribute('xmlns','jabber:x:delay') and \ x.hasAttribute('stamp') ] #stampstring = str( stamps[0].getAttribute('stamp') ) #timestamp = time.mktime( time.strptime(stampstring, "%Y%m%dT%H:%M:%S") ) if delays or stamps: log('discarding delayed message: %s' % e.toXml()) return # Route message to the right Conversation or ChatRoom entity if e.getAttribute('type') == 'chat': buddy = str(e['from'].split('/')[0]) if not Conversation.exists(buddy): self.requestAuthorization(buddy) log('received message from %s: %s' % (buddy.split('@')[0], message)) Conversation(buddy).hear(message) elif e.getAttribute('type') == 'groupchat': room = e['from'].split('@')[0] log('received message [chatroom=%s]: %s' % (room, message)) ChatRoom(room).hear(message) else: log('received message of unknown type: %s' % e.toXml(), error=True) def receivedPresence(self, e): log('received presence: %s' % e.toXml()) if e.getAttribute('type') == 'subscribe': log('received authorization request from %s' % e['from']) response = Element(('', 'presence')) response['to'] = e['from'] response['type'] = 'subscribed' log('sending auth response: %s' % response.toXml()) self.xmlstream.send(response) buddy = str(e['from']) if not Conversation.exists(buddy): self.requestAuthorization(buddy) elif e.getAttribute('type') == 'unavailable': #fix for openfire jabber server randomly kicking clients out and prevent kicks CHAT = '@%s/%s' % (self.SERVICECONFIG.JABBER_CHAT_SERVICE, self.SERVICECONFIG.JABBER_CHAT_NICK) if e['to'] == self.jid.full() and e['from'].endswith(CHAT) and \ "status code='307'" in e.toXml(): try: log('%s has kicked me' % (e['from'], )) self.joinChatRoom(e['from'].split(CHAT)[0]) log('successfully rejoined room') except: err('Failed to recover from /kick') #elif any(1 for c in e.children if c.name == 'x'): #TODO detect buddies that go offline # if we have a Conversation then unsubscribe .notify from all events def receivedIQ(self, e): log('received iq: %s' % e.toXml()) def receivedError(self, f): log('received error: %s' % str(f)) def initFailed(self, failure): log('Failed to initialize jabber connection:\n%s' % failure.getTraceback()) self.stop() if failure.check(SASLAuthError): log('Will attempt to reconnect in 15 seconds...') config.reactor.callLater(15, self.start)
class LiveProcess(Process): """Get realtime access to process information""" implements(IKittLiveProcess) pass
class LiveProcess(Process): """Get realtime access to process information""" implements(IKittLiveProcess) def __init__(self, pid, fast=False): """Represents the /proc entries for a process, values are read each time you access an attribute. """ Process.__init__(self, pid) #Creates dummy attribs for user friendliness unless fast=True is specified if not fast: other_attribs = ('cmdline', 'cwd', 'environ', 'exe', 'fd', 'root', 'tgid') dummyAttrs = dict([(a,self.__getattribute__(a)) for a in \ stat_attribs + other_attribs]) vars(self).update(dummyAttrs) self.cpuTime = self.__cpuSnapShot() def __times(self): """Returns seconds of system and user times""" return (float(self.utime) / JIFFIES_PER_SECOND, float(self.stime) / JIFFIES_PER_SECOND) def cpuUsage(self): """Returns a dictionary of system and user cpu utilization in terms of percentage used """ baseline = self.cpuTime self.cpuTime = self.__cpuSnapShot() u = (self.cpuTime[0] - baseline[0]) / (self.cpuTime[2] - baseline[2]) s = (self.cpuTime[1] - baseline[1]) / (self.cpuTime[2] - baseline[2]) return {'user_util': 100 * u, 'sys_util': 100 * s} def __cpuSnapShot(self): return self.__times() + tuple([cpuTotalTime()]) def __getattribute__(self, attr): if attr == 'cmdline': return self.readFile('cmdline').split('\000')[:-1] elif attr == 'cwd': try: return os.readlink('%s/cwd' % self.path) except: return None elif attr == 'environ': return self.getEnv() elif attr == 'exe': try: return os.readlink('%s/exe' % self.path) except: return None elif attr == 'fd': return self.getFD() elif attr == 'tasks': return self.getTasks() elif attr == 'root': try: return os.readlink('%s/root' % self.path) except: return None elif attr == 'tgid': return int( self.readFile('status').split('Tgid:', 1)[1].split(None, 1)[0]) elif attr in stat_attribs: return self.getStats()[attr] elif attr == 'uid': try: return os.stat(self.path).st_uid except: return None elif attr == 'gid': try: return os.stat(self.path).st_gid except: return None else: return object.__getattribute__(self, attr)
class LiveProcess(Process): """Get realtime access to process information""" implements(IKittLiveProcess) def __init__(self, pid, fast=False): Process.__init__(self, pid) #Creates dummy attribs for user friendliness unless fast=True is specified if not fast: other_attribs = ('cwd', 'environ', 'exe', 'fd', 'root', 'tasks') dummyAttrs = dict([(a,self.__getattribute__(a)) for a in \ stat_attribs + other_attribs]) vars(self).update(dummyAttrs) self.cpuTime = self.__cpuSnapShot() def __times(self): """Returns seconds of system and user times""" return (self.utime, self.stime) def cpuUsage(self): """Returns a dictionary of system and user cpu utilization in terms of percentage used @return (dict) """ baseline = self.cpuTime self.cpuTime = self.__cpuSnapShot() u = (self.cpuTime[0] - baseline[0]) / (self.cpuTime[2] - baseline[2]) s = (self.cpuTime[1] - baseline[1]) / (self.cpuTime[2] - baseline[2]) return { 'user_util': 100 * u, 'sys_util': 100 * s, } def __cpuSnapShot(self): #FIXME probably, not tested enough return self.__times() + tuple([(time.time() - self.time)]) def __getattribute__(self, attr): if attr == 'cwd': try: return os.readlink('%s/cwd' % self.path) except: return None elif attr == 'environ': return self.getEnv() elif attr == 'exe': self.fname[0] elif attr == 'fd': return self.getFD() elif attr == 'tasks': return self.getTasks() elif attr == 'cwd': try: return os.readlink('%s/path/cwd' % self.path) except: return None elif attr == 'root': try: return os.readlink('%s/path/root' % self.path) except: return None elif attr in stat_attribs: try: return self.getStats()[attr] except: pass else: return object.__getattribute__(self, attr)
class EntityContext(object): """Abstract base class for representing context about an Entity with read-only metadata transparently mixed in as "special keys". Subclasses must override entityAttr and specialKeys attributes and for each specialKey a get_<key> method must be implemented. """ implements(IEntityContext) entityAttr = None specialKeys = [] def __init__(self, entity): #Verify that the subclass conforms to the API assert self.entityAttr is not None for key in self.specialKeys: assert hasattr(self, 'get_' + key) setattr(self, self.entityAttr, entity) self.data = {} def __getitem__(self, key): if key in self.specialKeys: accessor = getattr(self, 'get_' + key) return accessor() if key in self.data: return self.data[key] else: entity = getattr(self, self.entityAttr) raise KeyError("\"%s\" is not in the %s context" % (key, entity)) def __setitem__(self, key, value): if key in self.specialKeys: raise KeyError("Cannot override special key \"%s\"" % key) self.data[key] = value def __delitem__(self, key): if key in self.specialKeys: raise KeyError("Cannot delete special key \"%s\"" % key) del self.data[key] def __contains__(self, key): return key in self.data or key in self.specialKeys def __iter__(self): for key in self.specialKeys: yield key for key in self.data: yield key def get(self, key, default=noDefault): try: return self[key] except KeyError: if default == noDefault: raise else: return default def pop(self, key, default=noDefault): try: return self.data.pop(key) except KeyError: if default == noDefault: raise else: return default def keys(self): return list(self) def values(self): return [self[key] for key in self] def items(self): return [(key, self[key]) for key in self] def update(self, otherDict): return self.data.update(otherDict) def clear(self): return self.data.clear() def copy(self): return dict(self.items()) def __repr__(self): return repr(self.copy())
class AppInstance(Entity): """Track application instances""" implements(IDroneModelAppInstance) crashed = property(lambda s: s.shouldBeRunning and not s.running) startupInstallInfo = {} runningConfigs = set() serializable = True state = property(lambda s: (s.crashed and 'crashed') or \ (s.running and 'up') or 'not running') description = property(lambda self: "%s %s [%s] on %s" % \ (self.app.name, self.version, self.label, self.server.hostname)) localInstall = property(lambda s: bool(s.server.hostname == \ config.HOSTNAME)) #FIXME broken cpu = property(lambda s: 0.0) #immutable objects passed to the constructor label = property(lambda s: s._label, lambda x: None, lambda y: None, 'instance label') app = property(lambda s: s._app, lambda x: None, lambda y: None, 'L{IDroneModelApp} provider') server = property(lambda s: s._server, lambda x: None, lambda y: None, 'L{IDroneModelServer} provider') def __getattribute__(self, name): """Overrode to fulfill our interface obligations""" if name in ('running','ppid','memory','fd_count','stats','threads',\ 'exe','environ','cmdline'): return self.process.__getattribute__(name) return object.__getattribute__(self, name) def __init__(self, server, app, label): try: if not IDroneModelServer.providedBy(server): e = '%s is not a L{IDroneModelServer} provider' % str(server) raise AssertionError(e) if not IDroneModelApp.providedBy(app): e = '%s is not a L{IDroneModelAppVersion} provider' % \ str(appversion) raise AssertionError(e) except AssertionError: AppInstance.delete(self) raise #internal information self.shouldBeRunning = False #model information self._label = label self._app = IDroneModelApp(app) self._server = IDroneModelServer(server) #serializable information self.info = {} #volitile data, unserializable self.context = {} @property def children(self): """allow us to track an AppInstance's child processes""" if IDroneModelAppProcess.providedBy(self.process): return self.process.children #generator return (i for i in []) #empty generator @property def process(self): """The process object contained by this application instance @raise InvalidProcess @return (instance of AppProcess) """ if not hasattr(self, '_process'): #try to grab a live process, on exception grab a NullProcess try: assert self.pid #save the scan attempt self._process = IDroneModelAppProcess(self) except: self._process = IKittNullProcess( self) #should be a NullProcess elif IDroneModelAppProcess.providedBy(self._process) and not \ AppProcess.isValid(self._process): self._process = IKittNullProcess(self) #keep the journal up to date self.info.update({ 'pid': self._process.pid, 'inode': self._process.inode, }) return self._process def __getstate__(self): state = { 'server': self.server.hostname, 'app': self.app.name, #avoid journal races 'pid': self.info.get('pid', 0), 'inode': self.info.get('inode', 0), 'version': self.version, #for proper serialization 'label': self.label, 'shouldBeRunning': self.shouldBeRunning, 'enabled': self.enabled, 'running': self.running, 'info': {}, #other information storage } for attr, val in self.info.items(): if attr in ('pid', 'inode', 'enabled'): continue state['info'][attr] = val return state @staticmethod def construct(state): server = Server(state['server']) #we need to format the version correctly appname = state['app'] version = AppVersion.makeAppVersion(appname, state['version']) appinstance = AppInstance(server, App(appname), state['label']) appinstance.appversion = version pid = state.get('pid', 0) inode = state.get('inode', 0) from kitt.proc import isRunning if pid and isRunning(pid): appinstance.info.update({'pid': pid, 'inode': inode}) process = AppProcess(server, pid) if process.inode == inode: setattr(appinstance, '_process', process) appinstance.updateInfo(state['info']) appinstance.enabled = state.get('enabled', False) appinstance.shouldBeRunning = state.get('shouldBeRunning', False) x = appinstance.running #preload the process information #attempt to get our instance into the last known state return appinstance def updateInfo(self, info): """Called by app managers after a start/stop condition""" result = info if isinstance(info, Failure): info = info.check(DroneCommandFailed) if info: info = info.resultContext if not isinstance(info, dict): return result #don't allow dumb changes info.pop('pid', None) info.pop('inode', None) self.info.update(dict(**info)) return result @defer.deferredGenerator def start(self): """convenient start method for an Application Instance""" result = None app = self.app.name label = self.label d = self.server.manager.run("%(app)s start %(label)s" % locals()) wfd = defer.waitForDeferred(d) yield wfd result = wfd.getResult().values()[0] self.shouldBeRunning = True if self.server.hostname != config.HOSTNAME: self.updateInfo(result) if not self.running: result = Failure(DroneCommandFailed(result)) yield result @defer.deferredGenerator def stop(self): """convenient stop method for an Application Instance""" result = None app = self.app.name label = self.label d = self.server.manager.run("%(app)s stop %(label)s" % locals()) wfd = defer.waitForDeferred(d) yield wfd result = wfd.getResult().values()[0] self.shouldBeRunning = False if self.server.hostname != config.HOSTNAME: self.updateInfo(result) if self.running: result = Failure(DroneCommandFailed(result)) yield result @defer.deferredGenerator def restart(self): """convenient restart method for an Application Instance""" result = None try: if self.running: d = self.stop() wfd = defer.waitForDeferred(d) yield wfd wfd.getResult() d = self.start() wfd = defer.waitForDeferred(d) yield wfd result = wfd.getResult() except: result = Failure() log('Unhandled exception\n' + result.getTraceback()) yield result ########################################################################### # getting and setting of attr's ``pid``, ``inode``, ``version``, and # ``appversion`` are done below here. ########################################################################### def _getpid(self): pid = int(self.info.get('pid', 0)) if hasattr(self, '_process') and (self.process.pid != pid): delattr(self, '_process') #force rescan for process pid = self.info['pid'] = 0 return pid def _getinode(self): inode = int(self.info.get('inode', 0)) if hasattr(self, '_process') and (self.process.inode != inode): delattr(self, '_process') #force rescan for process inode = self.info['inode'] = 0 return inode def _setpid(self, pid): pid = int(pid) if hasattr(self, '_process') and (self.process.pid != pid): delattr(self, '_process') #force update on change self.info['pid'] = pid return self.info['pid'] def _setinode(self, inode): inode = int(inode) if hasattr(self, '_process') and (self.process.inode != inode): delattr(self, '_process') #force update on change self.info['inode'] = inode return self.info['inode'] def _getversion(self): if not hasattr(self, '_version'): self._version = AppVersion.makeAppVersion(self.app.name, None) return self.appversion.version_string def _getappversion(self): if not hasattr(self, '_version'): self._version = AppVersion.makeAppVersion(self.app.name, None) return IDroneModelAppVersion(self._version) def _setversion(self, version): """sets the self.appversion and self.version""" checkVersion = hasattr(self, '_version') #could be reconstructing if checkVersion: checkVersion = self._getappversion() if IDroneModelAppVersion.providedBy(version): self._version = IDroneModelAppVersion(version) else: self._version = IDroneModelAppVersion( AppVersion.makeAppVersion(self.app.name, version)) if checkVersion: data = { 'instance': self, 'version': self._version, 'previous': checkVersion } if checkVersion < self._version: if checkVersion.major < self._version.major: Event('new-major-release').fire(**data) else: Event('new-release-version').fire(**data) return #done Event('release-change').fire(**data) def _getenabled(self): return self.info.get('enabled', False) def _setenabled(self, enabled): enabled = bool(enabled) status = self._getenabled() self.info['enabled'] = enabled if (enabled != status) and enabled: Event('instance-enabled').fire(instance=self) elif (enabled != status) and not enabled: Event('instance-disabled').fire(instance=self) #dynamically changing properties that are special to the system pid = property(_getpid, _setpid, lambda x: None, 'process id') inode = property(_getinode, _setinode, lambda x: None, 'process inode') version = property(_getversion, _setversion, lambda x: None, 'App Version String') appversion = property(_getappversion, _setversion, lambda x: None, 'L{IDroneModelAppVersion} provider') enabled = property(_getenabled, _setenabled, lambda x: None, 'instance enabled status')
class AdminAction(Entity): """Slick interface to invoke exposed methods via blaster protocol Requirement: droned.services.drone must be running to access methods that are exposed via this interface, this is configurable in DroneD's config.py settings. Developer Notes: how to use this interface ... 1) instantiate with the name of the "action" you wish to expose 2) expose methods see AdminAction.expose 3) build documentation ... call AdminAction.buildDoc Examples: code: foo = AdminAction('bar') foo.expose('baz', lambda: 'hello world' (), 'example of AdminAction') foo.buildDoc() admin: #shell: << droneblaster bar ### or ### droneblaster help bar #shell: >> 127.0.0.1:5500 -> -4: "Usage: bar <command> [options] #shell: >> #shell: >> foo example of AdminAction #shell: >> " #shell: >> Run Time: 0.019 seconds INVOCATION #shell: << droneblaster bar foo #shell: >> 127.0.0.1:5500 -> 0: "hello world" #shell: >> Run Time: 0.017 seconds """ implements(IDroneModelAdminAction) serializable = False def __init__(self, action): self.action = action self.exposedMethodInfo = [] self.exposedMethods = {} def log(self, message): """where to send logging information""" logWithContext(type=self.action, route='console')(str(message)) def buildDoc(self): """You might need this, so it is provided. Rebuilds help <action>""" self.__doc__ = "Usage: %s <command> [options]\n\n" % (self.action, ) for name, args, doc in self.exposedMethodInfo: argStr = ' '.join(['<' + arg + '>' for arg in args]) self.__doc__ += " %s %s\t%s\n" % (name, argStr, doc) #FIXME, document this better and clean it up def resultContext(self, template, instance=None, **context): """Creates a dict containg relevant contextual information about a result. You can override this method and tailor it to your liking. We typically use this to pass verbose structured data to a master DroneD controller (not provided with DroneD core) so that it may quickly make decisions based on the result of it's previous command and control activities. IF you set 'error' in the **context this will raise a server error at the remote end. This can be good or bad depending on your outlook on exceptions. Consider this your only warning. return dict """ if 'application' not in context: context['application'] = self.action failure = context.pop('error', False) if isinstance(failure, Failure): if 'description' not in context: context['description'] = '[%s] %s: %s' % \ (self.action, getException(failure), failure.getErrorMessage()) if 'code' not in context: context['code'] = -2 context['error'] = True context['stacktrace'] = failure.getTraceback() self.log('Result context during exception\n%(stacktrace)s' % context) return context #failed so bad we need to shortcut out else: context['error'] = bool(failure) if instance: #this was made for AppManager's if hasattr(instance, 'version'): context['version'] = instance.version if hasattr(instance, 'label'): context['label'] = instance.label if hasattr(instance, 'running'): context['running'] = instance.running try: #fail-safe in case someone is a bonehead context['description'] = template % context except: failure = Failure() context['description'] = '[%s] %s: %s' % \ (self.action, getException(failure), failure.getErrorMessage()) context['stacktrace'] = failure.getTraceback() if 'code' not in context: context['code'] = -2 #be nice to blaster api and the remote client context.update({'code': context.get('code', 0)}) return context def invoke(self, name, args): """Invoke Exposed Methods @param name (str) - name of method to invoke @param args (tuple) - arguments to pass to invoked method @return (defer.Deferred) """ if name not in self.exposedMethods: return defer.fail( DroneCommandFailed( self.resultContext( "[%(application)s] Unknown method '%(method)s'", method=name, error='unknown method'))) try: #our own form of maybeDeferred d = self.exposedMethods[name](*args) if isinstance(d, defer.Deferred): action = Action(' '.join([str(i) for i in \ (self.action, name) + tuple(args)]), d) return action.deferred elif isinstance(d, DroneCommandFailed): return defer.fail(d) elif isinstance(d, dict): return defer.succeed(d) elif isinstance(d, type(None)): #this just feels dirty return defer.succeed(d) elif isinstance(d, Failure): d.raiseException() #sigh #probably from a triggerred Event callback elif type(d) == types.InstanceType: return defer.succeed(None) return defer.fail(FormatError("Result is not formatted correctly you " + \ "must return self.resultContext or DroneCommandFailed." + \ "\nResult: <%s>" % (str(d),))) except: failure = Failure() if failure.check(DroneCommandFailed): template = "[%(application)s] %(description)s" context = failure.value.resultContext if not 'description' in context: context['description'] = failure.getErrorMessage() else: template = "[%(application)s] " + "%s: %s" % ( getException(failure), failure.getErrorMessage()) context = { 'error': True, 'code': -2, 'stacktrace': failure.getTraceback() } return defer.fail( DroneCommandFailed( self.resultContext(template, None, **context))) @defer.deferredGenerator def __call__(self, argstr): args = argstr.split() resultContext = None if not args: #return command usage methods = {} for name, args, doc in self.exposedMethodInfo: methods[name] = {'args': args, 'doc': doc} resultContext = dict(description=self.__doc__, methods=methods) yield resultContext else: method = args.pop(0) try: wfd = defer.waitForDeferred(self.invoke(method, args)) yield wfd resultContext = wfd.getResult() except: failure = Failure() if failure.check(DroneCommandFailed): resultContext = failure.value.resultContext else: #be nice and return something to the end user template = "[%(application)s] " template += "%s: %s" % (getException(failure), failure.getErrorMessage()) context = { 'error': True, 'code': -2, 'stacktrace': failure.getTraceback() } resultContext = self.resultContext(template, None, **context) yield resultContext def expose(self, name, method, args, doc): """Exposes a method in 'self.action name *args' to the class droned.services.drone.DroneServer for remote invocation over the blaster protocol @param name: (string) @param method: (callable) @param args: tuple((string), ...) -> named positional arguments @param doc: (string) -> administative documentation .. (ie usage) @return None """ if name in self.exposedMethods: raise AttributeError('method %s is already reserved in %s' % \ (name, str(self))) self.exposedMethodInfo.append((name, args, doc)) self.exposedMethods[name] = method def unexpose(self, name): """Removes an exposed method @param name: (string) @return None """ #check the method dictionary first if name in self.exposedMethods: del self.exposedMethods[name] info = None #locally scoped try: #make sure the documentation is up to date for info in self.exposedMethodInfo: if info[0] != name: continue raise StopIteration('Found Method') except StopIteration: self.exposedMethodInfo.remove(info) @staticmethod def byName(action): for obj in AdminAction.objects: if obj.action == action: return obj return None
class AppManager(Entity): """This is a generic application container service. It's sole purpose is to provide an abstraction to the application plugin. Think of this as an application service container. """ implements(IDroneModelAppManager) serializable = True #global container lock globalLock = defer.DeferredLock() running = property(lambda s: hasattr(s, '_task') and s._task.running) model = property(lambda s: IDroneDApplication(s)) #late plugin lookup action = property(lambda s: AdminAction(s.name)) invoke = property(lambda s: s.action.invoke) resultContext = property(lambda s: s.action.resultContext) exposedMethodInfo = property(lambda s: s.action.exposedMethodInfo) exposedMethods = property(lambda s: s.action.exposedMethods) instances = property(lambda s: App(s.name).localappinstances) labels = property(lambda s: (i.label for i in s.instances)) #whether or not the application service should discover apps for us discover = property(lambda s: not all([i.running for i in s.instances])) def __init__(self, name): self.name = name #this is for user defined storage self.applicationContext = {} #allow the models to block methods from registering self.blockedMethods = set() #create a local lock self.busy = defer.DeferredLock() #track events self.events = {} def log(self, message, label=None): """route logging messages to the application log and allow for custom labeling to be applied @param message: (string) @param label: (string) or (None) @return None """ info = self.name if label: info += ',%(label)s' % locals() logWithContext(type=info, route='application')(message) def __getstate__(self): """used to serialize the application model""" return { 'name': self.name, 'applicationContext': self.applicationContext } @staticmethod def construct(state): """rebuild the model with context @param state: (dict) return AppManger(state['name']) """ manager = AppManager(state['name']) manager.applicationContext = state['applicationContext'] return manager def start(self): """This is used by service binding to start""" if self.running: raise AssertionError('already running') #not only is this a safety, but makes sure the model is bound #donot ever remove this, otherwise first run won't automatically #create appinstances or any other models. #should be self, but make sure we avoid a race if self.model.service != AppManager(self.name): raise InvalidPlugin('Plugin for %s is invalid' % (self.name, )) self.action.log = self.log #override default logging #create default exposed methods, the model can override any of these self.expose('add', self.addInstance, ('instance', ), "Configure the specified instance", BUSYLOCK=True) self.expose('remove', self.removeInstance, ('instance', ), "Unconfigure the specified instance", BUSYLOCK=True) self.expose('start', self.startInstance, ('instance', ), "Start the instance", BUSYLOCK=True, INSTANCED=True) self.expose('stop', self.stopInstance, ('instance', ), "Stop the instance", BUSYLOCK=True, INSTANCED=True) self.expose('status', self.statusInstance, ('instance', ), "Status the instance", INSTANCED=True) self.expose('enable', self.enableInstance, ('instance', ), "Enable the instance", INSTANCED=True) self.expose('disable', self.disableInstance, ('instance', ), "Disable the instance", INSTANCED=True) self.expose('debug', self.debug, ('bool', ), "Turn application container debugging on or off") self.expose( 'labels', lambda: self.resultContext('\n'.join(sorted(self.labels)), None, ** {'labels': sorted(self.labels)}), (), "lists all application instance labels") #build our documentation self.rebuildHelpDoc() #check conditional events self._task = LoopingCall(self.conditionalEvents) self._task.start(1.0) def conditionalEvents(self): """check the status of conditional events""" if self.busy.locked: return #skip conditional event processing while busy for appevent in self.events.values(): if not appevent.condition: continue appevent.occurred() def registerEvent(self, name, callback, **kwargs): """Interface to Register Service Events""" #the self parameter will help ensure this event is unique to the service self.events[name] = ApplicationEvent(self.name, name, callback, **kwargs) def triggerEvent(self, name, data=None, delay=0.0): """Interface to trigger an out of band service event""" assert name in self.events, "No such event '%s'" % (name, ) return self.events[name].trigger(data, delay) def disableEvent(self, name): """Interface to disable a previously registered service event""" assert name in self.events, "No such event '%s'" % (name, ) self.events[name].event.disable() def enableEvent(self, name): """Interface to enable a previously disabled registered service event """ assert name in self.events, "No such event '%s'" % (name, ) self.events[name].event.enable() def stop(self): """This is used by service binding to stop""" try: if not self.running: raise AssertionError('not running') self._task.stop() #clear the event dictionary and delete events while self.events: name, appevent = self.events.popitem() if appevent.loop and appevent.loop.running: appevent.loop.stop() ApplicationEvent.delete(appevent) except: self.debugReport() #remove this appmanager's actions AdminAction.delete(self.action) ########################################################################### # This part of the class exposes the Model API to outside world ########################################################################### @synchronizedDeferred(globalLock) def unexpose(self, name, blacklist=True): """Removes an exposed method, probably not a good idea to expose""" #add method to blocked list if blacklist: self.blockedMethods.add(name) if name in self.exposedMethods: del self.exposedMethods[name] info = None found = False for info in self.exposedMethodInfo: (n, a, d) = info if n == name: found == True break if info and found: self.exposedMethodInfo.remove(info) def rebuildHelpDoc(self): """rebuild exposed method documentation""" self.action.buildDoc() @synchronizedDeferred(globalLock) def expose(self, name, method, args, doc, **kwargs): """Wraps the models exposed methods for gremlin and make methods available via blaster protocol for action invocation. expose(self, name, method, methodSignature, description, **kwargs) name: (string) - This is the action parameter to expose method: (callable) - This is the function name to call args: (tuple) - layout for parsing args description: (string) - Help Documentation to expose kwargs: INSTANCED: (bool) - sets the instanceOperator decorator for administrator's ease of use. BUSYLOCK: (bool) - sets the synchronizedDeferred decorator for this AppManager. GLOBALLOCK: (bool) - sets the synchronizedDeferred decorator for synchronizing all AppManagers. """ if name in self.blockedMethods: return #method was blocked by the model, probably #allow models to override the defaults and print a warning if name in self.exposedMethods: self.log('Warning method "%s" is already exposed' % (name, )) return #These decorators must be applied in a specific order of precedence requireInstance = kwargs.pop('INSTANCED', False) requireBusyLock = kwargs.pop('BUSYLOCK', False) requireGlobalLock = kwargs.pop('GLOBALLOCK', False) #applying decorators at runtime if requireBusyLock or requireGlobalLock or requireInstance: #ordering is critical if requireInstance: #this bizarre decorator is used b/c we need instance info. method = self.instanceOperation(method) if requireBusyLock: sync = synchronizedDeferred(self.busy) method = sync(method) if requireGlobalLock: sync = synchronizedDeferred(self.globalLock) method = sync(method) self.exposedMethodInfo.append((name, args, doc)) self.exposedMethods[name] = method ########################################################################### # This part of the class is for Generic actions that all apps perform ########################################################################### #FIXME is this really needed? def debug(self, var): """Enable or Disable application model debugging. You should extend this if you know how to enable application debugging in your custom 'application model'. returns deferred - already called """ #assume blaster which is string based, sent the message var = str(var) #for safety a = var.lower() context = {'code': 0} template = '[%(application)s] Debug ' try: if a == 'true': self.model.debug = True defer.setDebugging(True) template += 'Enabled' elif a == 'false': self.model.debug = False defer.setDebugging(False) template += 'Disabled' else: raise TypeError('input must be a bool, True/False') except Exception, exc: template += str(exc) context['code'] = 1 return defer.succeed(self.resultContext(template, None, **context))
class ApplicationPlugin(object): """Basis for DroneD Application Management. There are some expectations that you must meet for this to work. All started apps must daemonize ie fork away from DroneD. The protocol must return the application pid or a failure if it does not the call will be cancelled and your app will terminate. take a look "__init__" to get an idea on the attributes that need to be setup, defaults are provided. read documentation of twisted.internet.reactor.spawnProcess and read droned.clients.__init__.command to see how it is being used. you need to define the following in your implementation of this class. name: (str) should be the name of your application #how to manage your application STARTUP_INFO: (dict) SHUTDOWN_INFO: (dict) #protocols to handle your application startProtocol: (class(droned.protocols.application.ApplicationProtocol)) stopProtocol: (class(droned.protocols.application.ApplicationProtocol)) #arguments to protocols startProtoArgs: (tuple) stopProtoArgs: (tuple) #keyword arguments to protocols startProtoKwargs: (dict) stopProtoKwargs: (dict) #further reading ... see droned.applications.ApplicationPlugin.__init__ and see droned.applications.ApplicationStorage.__init__ for the implementation specific details. This class and its derivatives use a metaclass to add storage capabilities. also note that some of the instance methods are actually implemented inside of the metaclass. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! All plugins become singletons based on the ``class`` and ``name``. If you are extending another plugin class definition DONOT call it's __init__ method. MRO and Singleton pattern will cause you hell. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! """ implements(IDroneDApplication) def __init__(self, *args, **kwargs): """The DroneD Service that loads ApplicationPlugins DOES NOT pass custom arguments or keyword parmeters to the constructor. So you should not create constructors that expect external configuration. If you need external configuration you should implement a library to use for this purpose. Default settings are provided in '__init__'. #all of these settings are required #provided by the metaclass as well #self.name = name #self.INITIALIZED = False #the metaclass provides these as defaults # self.STARTUP_INFO = { # 'START_USEPTY' : 0, # 'START_CHILDFD' : {0:'w',1:'r',2:'r'}, # 'START_ENV' : {}, # 'START_PATH' : None, # 'START_CMD' : '/bin/true', # 'START_ARGS' : (), # } # self.SHUTDOWN_INFO = { # 'STOP_USEPTY' : 0, # 'STOP_CHILDFD' : {0:'w',1:'r',2:'r'}, # 'STOP_ENV' : {}, # 'STOP_PATH' : None, # 'STOP_CMD' : '/bin/true', # 'STOP_ARGS' : (), # } # #these protocol settings must be defined the metaclass will #provide sane defaults, that work for 90% of use cases # # self.startProtocol = ApplicationProtocol # self.startProtoArgs = () #protocol constructor *args # self.startProtoKwargs = {} #protocol constructor **kwargs # # self.stopProtocol = ApplicationProtocol # self.stopProtoArgs = () #protocol constructor *args # self.stopProtoKwargs = {} #protocol constructor **kwargs """ @defer.deferredGenerator def recoverInstance(self, occurance): """Recover Crashed Instances of the Application. this method should be subscribed to Event('instance-crashed') @param occurance: (object) @return defer.Deferred() """ #check to make sure this is one of our instances if occurance.instance.app.name == self.name: self.log('application crashed restarting') #by default go through the AppManager d = self.service.startInstance(occurance.instance.label) d.addCallback(lambda x: self.log('sucessfully restarted') and x) d.addErrback( lambda x: self.log('failed to recover from crash') and x) result = None try: wfd = defer.waitForDeferred(d) yield wfd yield wfd.getResult() except: failure = Failure() self.log('throttling restart attempts') d = defer.Deferred() self.reactor.callLater(10, d.callback, None) wfd = defer.waitForDeffered(d) yield wfd wfd.getResult() yield failure else: yield 'not my application instance' @defer.deferredGenerator def startInstance(self, label): """Starts an Application Instances based on our models rules @param label: (string) - app instance label @return defer.Deferred - be prepared for Failures() """ start = dictwrapper(copy.deepcopy(self.STARTUP_INFO)) #configure our protocol if 'debug' not in self.startProtoKwargs: self.startProtoKwargs['debug'] = False if 'timeout' not in self.startProtoKwargs: self.startProtoKwargs['timeout'] = self.DEFAULT_TIMEOUT if 'logger' not in self.startProtoKwargs: self.startProtoKwargs['logger'] = self.log #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # # !! READ THIS COMMENT BLOCK !! # # the callback result from your protocol should return a dictionary # with a KEY 'pid' included and 'pid' should be an integer otherwise # the injected callback immediately following 'command' will fail to # update your instance state. You have been warned. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! result = None try: thisInst = self.getInstance(label) TIME = str(time.time()) anchor = DIGEST_INIT() anchor.update(self.name) anchor.update(label) anchor.update(TIME) #add some randomness anchor = str(anchor.hexdigest()) #inject some env variables that we may want to retrieve later ENV = { 'DRONED_IDENTIFIER': anchor, 'DRONED_STARTTIME': TIME, 'DRONED_LABEL': label, 'DRONED_APPLICATION': self.name, 'DRONED_LOGDIR': config.LOG_DIR, } if thisInst.version: ENV['DRONED_VERSION'] = thisInst.version #add these vars to the start env of the contained application start.START_ENV.update(ENV) d = command(start.START_CMD, start.START_ARGS, start.START_ENV, start.START_PATH, start.START_USEPTY, start.START_CHILDFD, self.startProtocol, *self.startProtoArgs, **self.startProtoKwargs) wfd = defer.waitForDeferred(d) yield wfd result = wfd.getResult() #we probably might not know the pid yet #if allowed by config search for the instance after some delay pid = result.get('pid', 0) #just in case the protocol knows it if isinstance(self.SEARCH_DELAY, (int, float)) and not pid: d = defer.Deferred() self.reactor.callLater(self.SEARCH_DELAY, d.callback, None) wfd = defer.waitForDeferred(d) yield wfd wfd.getResult() #don't care about this result d = self.findProcesses() wfd = defer.waitForDeferred(d) yield wfd data = wfd.getResult() if data: data = data.pop(0)[1] #we are just going to take the first result.update(data) #we should have the pid captured #this is the proper way to notify the AppInstance that we are running thisInst.pid = int(result.get('pid', 0)) except: result = Failure() yield result #NOTE runs hot due to IO read on Linux and Solaris @deferredAsThread def findProcesses(self): """Attempt to find a process by an ASSIMILATION pattern. This is a relatively naive attempt to find an application that works in most cases. NOTE: If your ASSIMILATION pattern includes group matches the dictionary will be updated with the output of ``groupdict() from re.search``. If droned is able to read the environment settings from the application that will be inlcuded in the result dictionary as well. @callback (list) - sorted([(int('PID'), dict), ...]) @errback (twisted.python.failure.Failure()) @return defer.Deferred() """ candidates = {} def safe_process(pid): try: #because processes can be invalid return AppProcess(Server(config.HOSTNAME), pid) except: err('problem') return None if self.PROCESS_REGEX: #rescan the whole system for processes for process in (safe_process(pid) for pid in listProcesses()): try: if not process: continue if not process.__class__.isValid(process): continue if process.pid in candidates: continue if process.ppid != 1: continue #droned wants your daemons if not process.running: continue if not process.localInstall: continue if process.managed: continue #already managed cmd = ' '.join(process.cmdline) if not cmd: continue match = self.PROCESS_REGEX.search(cmd) if not match: continue #remember we tried to set some VARS on startInstance _result = dict(**process.environ) #allows us to set interesting parameters in the regex _result.update(match.groupdict()) _result.update({'pid': process.pid}) candidates[_result['pid']] = _result except: self.log('error searching for process check console log') err('error searching for process') return sorted([(pid, d) for (pid, d) in candidates.items() if pid > 1]) @defer.deferredGenerator def assimilateProcess(self, information): """Naive assimilation method. This should work for standard single instance applications, it will attempt to work with multi-instance applications as well. You should consider overriding this in an ApplicationPlugin if you need more advanced strategies. This is used by the ``services.application`` module to assimilate rogue application instances. This makes a best guess of which instance this should be assigned too. @param information (dict) "result of self.findPid" - required key "pid": (int) > 0 - optional key "name": (str) self.name == name - optional key "label": (str) bind to this instance - optional key "version": (str) set version or promote version NOTES ``information['label']`` if the instance is running already assimilation will fail @callback (instance of droned.models.app.AppInstance or None) @return (instance of defer.Deferred) """ result = None pid = information.get('pid', 0) #droned attempted to inject these environment variables into the app name = information.get( 'name', information.get('DRONED_APPLICATION', self.name)) version = information.get('version', information.get('DRONED_VERSION', None)) label = information.get('label', information.get('DRONED_LABEL', None)) try: assert pid #process is dead assert App.exists(name) #no such app if label: #appinstance label is known thisInst = self.getInstance(label) #may throw AssertionError assert not thisInst.running #make sure this instance isn't running if bool(version) and (thisInst.version != version): thisInst = self.setVersion(label, version) thisInst.pid = pid result = thisInst raise Exception('assimilated process') else: #make a best guess attempt options = set() for ai in App(name).localappinstances: if ai.running: continue options.add(ai) if bool(version): #try to perform a version match for opt in options: if opt.version == version: opt.pid = pid result = opt raise Exception('assimilated process') #last ditch effort, pick lowest free container thisInst = sorted([i for i in options if not i.running])[0] if bool(version) and (thisInst.version != version): thisInst = self.setVersion(thisInst.label, version) thisInst.pid = pid result = thisInst raise Exception('assimilated process') except: pass #swallow errors #minor cool down period d = defer.Deferred() self.reactor.callLater(0.1, d.callback, result) wfd = defer.waitForDeferred(d) yield wfd result = wfd.getResult() yield result def stopInstance(self, label): """Stops an Application Instances based on our models rules @param label: (string) - app instance label @return defer.Deferred - be prepared for Failures() """ stop = dictwrapper(self.SHUTDOWN_INFO) #configure our protocol if 'debug' not in self.stopProtoKwargs: self.stopProtoKwargs['debug'] = False if 'timeout' not in self.stopProtoKwargs: self.stopProtoKwargs['timeout'] = self.DEFAULT_TIMEOUT if 'logger' not in self.stopProtoKwargs: self.stopProtoKwargs['logger'] = self.log return command(stop.STOP_CMD, stop.STOP_ARGS, stop.STOP_ENV, stop.STOP_PATH, stop.STOP_USEPTY, stop.STOP_CHILDFD, self.stopProtocol, *self.stopProtoArgs, **self.stopProtoKwargs) def setVersion(self, label, version): """sets the version of this instance I attempt to delegate to the original provider. @param label: (string) @param version: (string) @return AppInstance() """ version = AppVersion.makeVersion(self.name, version) thisInst = None thisInst = self.getInstance(label) if thisInst.version != version: thisInst.version = version return thisInst def addInstance(self, label): """add a new application instance optionally changing the version I attempt to delegate to the original provider. @param label: (string) @return AppInstance() """ label = str(label) return AppInstance(Server(config.HOSTNAME), App(self.name), label) def delInstance(self, label): """delete an application instance I attempt to delegate to the original provider. @param label (string) @return None """ label = str(label) thisInst = self.getInstance(label) if thisInst.running: raise AssertionError('cannot delete running application instance') AppInstance.delete(thisInst) def statusInstance(self, label): """Report the status of our Application Instance""" myInst = self.getInstance(label) result = { 'name': self.name, 'label': myInst.label, 'enabled': bool(myInst.enabled), 'running': myInst.running, 'version': myInst.version, 'pid': myInst.pid, 'ppid': myInst.ppid, 'inode': myInst.inode, 'crashed': bool(myInst.crashed), 'threads': myInst.threads, 'memory': int(myInst.memory), 'files': myInst.fd_count, 'cpu': float(myInst.cpu), } state = str(getattr(myInst, 'state', 'unknown')) description = '%s is %s.' % (myInst.description, state) result['state'] = state result['description'] = description return result def getInstance(self, label): """get a reference to the application instance I attempt to delegate to the original provider. @param label: (string) @return AppInstance() @exception AssertionError """ label = str(label) if AppInstance.exists(Server(config.HOSTNAME), App(self.name), label): return AppInstance(Server(config.HOSTNAME), App(self.name), label) raise AssertionError('no such application instance') def expose(self, *args, **kwargs): """expose methods via the L{IDroneModelAppManager} provider""" return self.service.expose(*args, **kwargs) def unexpose(self, *args, **kwargs): """unexpose methods via the L{IDroneModelAppManager} provider""" return self.service.unexpose(*args, **kwargs) def log(self, *args, **kwargs): """log messages via the L{IDroneModelAppManager} provider""" return self.service.log(*args, **kwargs)
class Process(_SunProc): """base class for processes""" implements(IKittProcess) running = property(lambda s: s.isRunning()) memory = property(lambda s: s.memUsage()) stats = property(lambda s: s.getStats()) environ = property(lambda s: s.getEnv()) threads = property(lambda s: len(s.getTasks())) fd_count = property(lambda s: len(s.getFD()) or 3) def __init__(self, pid): self.pid = int(pid) self.path = "%s/%d" % (PROCDIR, int(self.pid)) if not os.path.isdir(self.path): #this exception is injected into the module on import raise AssertionError("Invalid PID (%s)" % pid) self.inode = os.stat(self.path).st_ino _SunProc.__init__(self) def isRunning(self): """is a process running @return (bool) """ try: if self.pid > 0: #in case the process is an unreaped child os.waitpid(self.pid, os.WNOHANG) except: pass try: return os.stat(self.path).st_ino == self.inode except: return False def waitForDeath(self, timeout=10, delay=0.25): """wait for process to die @return (bool) """ while timeout > 0: if not self.isRunning(): return True time.sleep(delay) timeout -= delay return False #FIXME def getEnv(self): """the environment settings from the processes perpective, @return (dict) """ env = {} return env def getFD(self): """Get all open file descriptors @return (dict) """ fd = {} try: #can't decide if we should 'path' or 'fd' for link in os.listdir('%s/fd' % self.path): try: fd[link] = os.readlink('%s/fd/%s' % (self.path, link)) except: pass except: pass return fd def getTasks(self): """Get all open tasks/threads @return (set) """ try: taskDir = os.path.join(self.path, 'lwp') if os.path.exists(taskDir): return set(map(int, os.listdir(taskDir))) else: return set() except: return set() def getStats(self): """Get the process' stats @return (dict) """ #get first dictionary x = self.readFile('psinfo') #get second dictionary y = self.readFile('status') #update first with contents of second return dict(x, **y) def memUsage(self): """Get the process' stats @return (dict) """ if not hasattr(self, 'size'): size = self.readFile('psinfo')['size'] self.size = size return (self.size * 1024) #b/c it is in Kb def __str__(self): return '%s(pid=%d)' % (self.__class__.__name__, self.pid) __repr__ = __str__
class Process(object): """base class for processes""" implements(IKittProcess) running = property(lambda s: s.isRunning()) memory = property(lambda s: s.memUsage()) stats = property(lambda s: s.getStats()) environ = property(lambda s: s.getEnv()) threads = property(lambda s: len(s.getTasks())) fd_count = property(lambda s: len(s.getFD()) or 3) def __init__(self, pid): self.pid = int(pid) self.path = "%s/%d" % (PROCDIR, pid) if not os.path.isdir(self.path): self.path = "%s/.%d" % (PROCDIR, pid) #For kernel 2.4 threads if not os.path.isdir(self.path): #this exception is injected into the module on import raise AssertionError("Invalid PID (%s)" % pid) self.inode = os.stat(self.path).st_ino def readFile(self, f): return open('%s/%s' % (self.path, f)).read() def isRunning(self): """is a process running @return (bool) """ try: if self.pid > 0: #in case the process is an unreaped child os.waitpid(self.pid, os.WNOHANG) except: pass try: return os.stat(self.path).st_ino == self.inode except: return False def waitForDeath(self, timeout=10, delay=0.25): """wait for process to die @return (bool) """ while timeout > 0: if not self.isRunning(): return True time.sleep(delay) timeout -= delay return False def getEnv(self): """the environment settings from the processes perpective, @return (dict) """ env = {} try: envlist = self.readFile('environ').split('\000') except: return env for e in envlist: if '=' in e: k, v = e.split('=', 1) else: k, v = e, '' k, v = k.strip(), v.strip() if not k: continue env[k] = v return env def getFD(self): """Get all open file descriptors @return (dict) """ fd = {} try: for link in os.listdir('%s/fd' % self.path): try: fd[link] = os.readlink('%s/fd/%s' % (self.path, link)) except: pass except: pass return fd def getTasks(self): """Get all open tasks/threads @return (set) """ try: if KERNEL26 or KERNEL3x: taskDir = os.path.join(self.path, 'task') if os.path.exists(taskDir): return set(map(int, os.listdir(taskDir))) else: return set() else: my_pid = self.pid #Attribute lookup is heavy for LiveProcess's my_threads = set() for entry in os.listdir(PROCDIR): if entry[0] != '.': continue try: pid = int(entry[1:]) thread = LiveProcess(pid, fast=True) if thread.tgid == my_pid: my_threads.add(pid) except: pass return my_threads except: pass return set() def getStats(self): """Get the process' stats @return (dict) """ stats = {} statstr = self.readFile('stat') begin, end = statstr.find('('), statstr.rfind(')') comm = statstr[begin + 1:end] statlist = [comm] + statstr[end + 2:].split(' ') for i in range(len(stat_attribs)): try: stats[stat_attribs[i]] = int(statlist[i]) except: try: stats[stat_attribs[i]] = statlist[i] except IndexError: pass #2.4 kernels don't have last 2 stat_attribs return stats def memUsage(self): """Returns resident memory used in bytes @return (int) """ return int(self.rss * PAGESIZE) def __str__(self): return '%s(pid=%d)' % (self.__class__.__name__, self.pid) __repr__ = __str__
class App(Entity): """track applications""" implements(IDroneModelApp) managedOn = property( lambda self: set(server for server in \ Server.objects if not server.unreachable and self in \ server.droned.apps) ) configuredOn = property( lambda self: set(i.server for i in \ self.appinstances) ) appversions = property( lambda self: (av for av in \ AppVersion.objects if av.app is self) ) appinstances = property( lambda self: (ai for ai in \ AppInstance.objects if ai.app is self) ) localappinstances = property( lambda self: (i for i in self.appinstances \ if i.server.hostname == config.HOSTNAME) ) runningInstances = property( lambda self: (i for i in self.appinstances \ if i.running) ) localrunningInstances = property( lambda self: (i for i in \ self.runningInstances if i.server.hostname == config.HOSTNAME) ) #rxContext = RxAppContextDescriptor() serializable = True @property def latestVersion(self): latest = None for av in self.appversions: if not latest: latest = av if av > latest: latest = av return latest def __init__(self, name): self.name = name self.shouldRunOn = set() def __getstate__(self): return { 'name': self.name, 'shouldRunOn': [server.hostname for server in self.shouldRunOn] } @staticmethod def construct(state): app = App(state['name']) app.shouldRunOn = set( Server(hostname) for hostname in \ state['shouldRunOn'] ) return app def runsOn(self, server): if server not in self.shouldRunOn: self.shouldRunOn.add(server) Event('app-servers-change').fire(app=self, server=server, change='added') def doesNotRunOn(self, server): if server in self.shouldRunOn: self.shouldRunOn.remove(server) Event('app-servers-change').fire(app=self, server=server, change='removed')
class AppProcess(Entity): """Track Processes that are associated with an AppInstance""" implements(IDroneModelAppProcess) created = property(lambda s: s._created) managed = property(lambda s: AppProcess.isValid(s) and \ isinstance(s.appinstance, AppInstance) and \ s.appinstance.__class__.isValid(s.appinstance)) # serializable = True localInstall = property(lambda s: bool(s.server.hostname == \ config.HOSTNAME)) pid = property(lambda s: s._pid) valid = property(lambda s: AppProcess.isValid(s) and s.running) def __getattribute__(self, name): """Overrode to fulfill our interface obligations""" if name in ('running','ppid','memory','fd_count','stats','threads',\ 'exe','environ','cmdline','inode'): try: return self.process.__getattribute__(name) except: delattr(self, '_process') #try one more time, just because we should return self.process.__getattribute__(name) #treat any failure like we are no longer running return object.__getattribute__(self, name) def __init__(self, server, pid): self._pid = pid self.server = IDroneModelServer(server) self._created = time.time() #don't set self._process try: try: #re-constructing, can cause problems with this if IKittNullProcess.providedBy(self.process.process): raise InvalidProcess("Invalid PID (%s)" % pid) except AttributeError: if isinstance(self.process, NullProcess): raise InvalidProcess("Invalid PID (%s)" % pid) raise #re-raise do avoid ending up in a pickle, literally except InvalidProcess: if config.HOSTNAME == self.server.hostname: AppProcess.delete(self) #make sure we are invalid raise InvalidProcess("Invalid PID (%s)" % pid) except IOError: #linux and solaris kitt.proc.LiveProcess use files AppProcess.delete(self) #make sure we are invalid raise InvalidProcess("Invalid PID (%s)" % pid) except: err('wtf happened here .. seriously i do not know!!!') AppProcess.delete(self) #make sure we are invalid raise @property def process(self): """@return L{IKittProcess} provider""" #work around constructor and allow a process to appear to die if not hasattr(self, '_process'): #adapt this model to a IKittProcess self._process = IKittProcess(self) elif self._process.pid != self.pid or not self._process.running: if config.HOSTNAME == self.server.hostname: AppProcess.delete( self) #take ourself out of serialization loop self._process = IKittNullProcess( self) #continue to work for any other refs return self._process @staticmethod def construct(state): server = Server(state.pop('server')) ap = None try: ap = AppProcess(server, state['pid']) except: return None if ap.localInstall and ap.inode != state['inode']: AppProcess.delete(ap) return None ap._created = state.pop('created') #we know this is going throw an adapter if IKittRemoteProcess.providedBy(ap.process.process): #remaining attributes go into the remote process ap.updateProcess(state) #this is only useful to remote processes return ap def __getstate__(self): #save enough state so that remote processes are useful #account for scabs and appinstances data = { 'created': self.created, 'managed': self.managed, 'server': self.server.hostname, #even scabs have this attr 'pid': self.pid, 'inode': self.inode, #used to determine if the pid is valid 'running': self.running, 'memory': self.memory, 'ppid': self.ppid, 'fd_count': self.fd_count, 'stats': self.stats, 'threads': self.threads, 'exe': self.exe, 'cmdline': self.cmdline } #return managed process return data @property def children(self): """emits a generator of our child process objects this is only really useful for scab detection this only works if the child pid has been contained in another AppProcess Instance. Of course it could also be useful in bizarre situations with apps that have a parent supervisor and child worker. """ for process in AppProcess.objects: if IKittNullProcess.providedBy(process.process): continue if not self.pid: break if process == self: continue if process.server.hostname != self.server.hostname: continue if process.ppid == self.pid and AppProcess.isValid(process): yield process @property def appinstance(self): """matches this AppProcess to an AppInstance @return L{IDroneModelAppInstance} provider or None """ try: return IDroneModelAppInstance(self) except: return None
class LiveProcess(object): """provide a droned compatible interface by delegating the heavy work to ``psutil`` if it is available. see kitt.interfaces.process.IKittProcess for a full api description. """ implements(IKittLiveProcess) ps = property(lambda s: s._delegate) running = property(lambda s: s.isRunning()) memory = property(lambda s: s.ps.get_memory_info().rss) inode = property(lambda s: s._saved_inode) fd_count = property(lambda s: len(s.getFD())) stats = property(lambda s: s.getStats()) environ = property(lambda s: s.getEnv()) cmdline = property(lambda s: s._cmdline) exe = property(lambda s: s._exe) uid = property(lambda s: s._uid) gid = property(lambda s: s._gid) pid = property(lambda s: s._pid) ppid = property(lambda s: s.ps.ppid) #could be reparented @property def threads(self): try: return self.ps.get_num_threads() except: return 1 def _make_exe(self): """work around access errors""" try: return self.ps.exe except: #work around not having any args to guess at try: return self.cmdline[0] #next best guess except IndexError: return self.ps.name #totally making this up now def _make_inode(self): """may not be supported on all systems so we do it ourselves""" #on linux and solaris, we could just check the inode of the #process as os.stat(/proc/%(PID)d).st_ino, but this is obviously #not portable, so we will just hash some static values from the #process and hope for the best. DroneD needs the inode to track #long running applications easily even if droned is down for #long periods of time. if HAS_PROC_DIR: #this is an optimization return os.stat(os.path.join(PROC_DIR, str(self.pid))).st_ino #this may use more IO than we like return hash((self.exe, self.pid, self.ps.create_time, self._name) + \ tuple(self.ps.cmdline)) & 0xffffffff #make sure return is positive def __init__(self, pid): if type(pid) != int: raise ValueError('Pid must be an integer') self._pid = pid self._delegate = psutil.Process(pid) #avoid looking these attr's up all of the time. #IO is expensive and the following attr's should be #static anyhow. self._name = self.ps.name self._cmdline = self.ps.cmdline self._uid = self.ps.uids.real self._gid = self.ps.gids.real self._exe = self._make_exe() #save this for running tests self._saved_inode = self._make_inode() if not self.running: raise AssertionError("Invalid PID (%d)" % self.pid) def isRunning(self): """make sure not only the pid is running but it is the same process we thought it was. this may be done naively with a simple hash. """ if HAS_PROC_DIR: #this is an optimization if not os.path.exists(os.path.join(PROC_DIR, str(self.pid))): return False elif not self.ps.is_running(): return False #make sure this is the same process we thought it was return bool(self._make_inode() == self.inode) def getEnv(self): """not portable so not implemented""" return {} def getFD(self): #every os has stdin, stdout, and stderr FDS = {0: None, 1: None, 2: None} try: FDS.update(dict((i.fd, i.path) for i in self.ps.get_open_files())) except: pass return FDS def getTasks(self): """get the thread id's""" try: return set(t.id for t in self.ps.get_threads()) except: return set() def getStats(self): """not portable so not implemented""" return {} def memUsage(self): """get memory usage in bytes""" return self.memory def waitForDeath(self, timeout=10, delay=0.25): """wait for the process to die""" #delay isn't needed, but it is part of the interface #definition, so we'll leave it as a dummy. try: self.ps.wait(timeout) except: pass #not sure if it is needed return not self.running def cpuUsage(self): cpu = self.ps.get_cpu_times() return {'user_util': cpu.user, 'sys_util': cpu.system} def __str__(self): return '%s(pid=%d)' % (self.__class__.__name__, self.pid) __repr__ = __str__
class ApplicationEvent(Entity): """The Extents Eventing For AppManagers""" implements(IDroneModelApplicationEvent) serializable = False reactor = property(lambda s: config.reactor) def __del__(self): if self.loop.running: self.loop.stop() try: Event.delete(self.event) except: pass #TODO document def __init__(self, service, name, callback, **kwargs): """ """ #get ready for python3 if not hasattr(callback, '__call__'): raise AssertionError("%s is not callable" % (callback.__name__, )) event_name = str(service) + '-' + str(name) self.event = Event(event_name) self.name = name self.loop = None self.service = service #borderline laziness on my part self.log = AppManager(service).log self.condition = kwargs.get('condition', None) self.recurring = float(kwargs.get('recurring', 0)) self.silent = kwargs.get('silent', False) assert not (self.recurring and self.condition), \ "recurring and condition args are mutually exclusive" self.func = callback.__name__ if not self.silent: self.event.subscribe(self.announce) #send data only if we have it, keeps compatibility with legacy api self.event.subscribe(lambda x: (hasattr(x, 'params') and x.params) \ and callback(x) or callback()) #our event loop is controlled by the Service if self.recurring > 0.0: self.loop = LoopingCall(self.event.fire) self.occurred() #automatically start re-occuring events def announce(self, *args, **kargs): """Anounce the Occurrence of an Event""" self.log("%s event occurred calling %s" % (self.name, self.func)) return True def occurred(self): """Check for Occurrence or Start Event Loop""" if not self.condition and self.loop and not self.loop.running: self.loop.start(self.recurring) elif self.condition and self.condition(): self.event.fire() return True return False #TODO document def trigger(self, data=None, delay=0.0): """Trigger an event, the return object can be cancelled""" kwargs = {} if data: kwargs['data'] = data return self.reactor.callLater(delay, self.event.fire, **kwargs)