# -*- test-case-name: twisted.test.test_sister -*- from twisted.spread.pb import Service, Perspective, Error from twisted.spread.sturdy import PerspectiveConnector from twisted.spread.flavors import Referenceable from twisted.spread.refpath import PathReferenceDirectory from twisted.internet import defer from twisted.python import log from twisted.python.failure import Failure from twisted.cred.util import challenge from twisted.cred.authorizer import DefaultAuthorizer from twisted.cred.identity import Identity class TicketAuthorizer(DefaultAuthorizer): def loadIdentity(self, identityName, keys): log.msg( "loading identity: %s:%s " %(identityName, keys)) ticket = challenge() ident = Identity(identityName, self.application) ident.setPassword(ticket) for serviceName, perspectiveName in keys: ident.addKeyByString( serviceName, perspectiveName) ident.save() return ticket True = 1 False = 0 class SisterMotherClient(Referenceable): def __init__(self, sistersrv): self.sistersrv = sistersrv def remote_loadResource(self, resourceType, resourceName, *args): return self.sistersrv.loadResource(resourceType, resourceName, args) def remote_callDistributed(self, srcResourceType, srcResourceName, destResourceType, destResourceName, methodName, args, kw): """invoked to call a method on a distributed object. """ resource = self.sistersrv.ownedResources.get( (destResourceType, destResourceName), None) if not resource: return defer.fail("Sister does not own this resource") method = getattr(resource, "sister_%s" % methodName) fullArgs = (srcResourceType, srcResourceName) + args return apply(method, fullArgs, kw) class SisterService(Service, Perspective): """A 'sister' object, managed by a mother server I am one of a set of sisters managed by a mother server. I use the mother server as a central manager of distributed objects, and as a mechanism for communicating to other sister servers. Distributed login into me is handled by passing identity resources - which I have a special loader and authorizer for. """ def __init__(self, motherHost, motherPort, motherService, publishHost, localPort, serviceName="twisted.sister", sharedSecret="shhh!", application=None): """Initialize me. (Application's authorizer must be a TicketAuthorizer, otherwise login won't work.) """ Service.__init__(self, serviceName, application) Perspective.__init__(self, "sister") self.addPerspective(self) self.ownedResources = {} self.remoteResources = {} self.resourceLoaders = {} self.localPort = localPort self.sisterMother = SisterMotherClient(self) self.motherRef = PerspectiveConnector( motherHost, motherPort, "mother", sharedSecret, motherService, client = self.sisterMother) self.makeIdentity(sharedSecret) self.application.authorizer.sisterService = self ## identities are a special kind of resource self.registerResourceLoader("identity", self.application.authorizer.loadIdentity) # this will be the first method called on the mother connection once it is setup self.motherRef.callRemote('publishIP', publishHost, self.localPort, self.sisterMother) def startService(self): log.msg( 'starting sister, woo') def __getstate__(self): d = self.__dict__.copy() d['ownedResources'] = {} d['remoteResources'] = {} return d # XXX I know what these mean, don't delete them -glyph def _cbLocked(self, result, path): if result is None: obj = apply(func, args, kw) self.ownedResources[path] = obj return (True, obj) else: self.remoteResources[path] = result return (False, result) def _ebLocked(self, error, path): log.msg('not locked, panicking') raise error # OK now on to the real code def ownResource(self, resourceObject, resourceType, resourceName): log.msg('sister: owning resource %s/%s' % (resourceType, resourceName)) self.ownedResources[resourceType, resourceName] = resourceObject return resourceObject def loadResource(self, resourceType, resourceName, args): """Returns a Deferred when the resource is loaded. This deferred may yield some data that is returned to the caller. """ log.msg( 'sister: loading resource %s/%s' %(resourceType, resourceName)) value = apply(self.resourceLoaders[resourceType], (resourceName,) + args) if isinstance(value, defer.Deferred): dvalue = value else: dvalue = defer.succeed(value) dvalue.addCallback(self.ownResource, resourceType, resourceName) return dvalue def registerResourceLoader(self, resourceType, resourceLoader): """Register a callable object to generate resources. The callable object may return Deferreds or synchronous values. """ log.msg( 'sister: registering resource loader %s:%s' % (resourceType, repr(resourceLoader))) self.resourceLoaders[resourceType] = resourceLoader def unloadResource(self, resourceType, resourceName): print "sister: unloading (%s:%s)" %(resourceType, resourceName) del self.ownedResources[ (resourceType, resourceName) ] return self.motherRef.callRemote("unloadResource", resourceType, resourceName).addCallback(self._cbUnload) def _cbUnload(self, data): log.msg( "Unloaded resource: %s" % data) def callDistributed(self, caller, destResourceType, destResourceName, methodName, *args, **kw): """Call a distributed method on a resource managed by the sister network. This will call the method 'getResourceInfo' on the calling object which must return its resourceType and resourceName to be identified by. The final method being called will have 'sister_' prepended to its name and have the calling objects resourceType and resourceName as the first arguments. #NOTE: this method of identifying the calling object is temporary.. need to establish a better way which includes allowing the calling object to expose some data and/or functionality to the caller. """ (srcResourceType, srcResourceName) = caller.getResourceInfo() if not self.ownedResources.has_key((srcResourceType,srcResourceName)): raise "sister does not own this resource!" fullArgs = ('callDistributed', srcResourceType, srcResourceName, destResourceType, destResourceName, methodName) + args return apply( self.motherRef.callRemote, fullArgs, kw) def removeIdentity(self, identityName): self.application.authorizer.removeIdentity(identityName) self.unloadResource("identity", identityName)