Package flumotion :: Package manager :: Module base
[hide private]

Source Code for Module flumotion.manager.base

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_manager_common -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 21   
 22  """ 
 23  common classes and code to support manager-side objects 
 24  """ 
 25   
 26  from twisted.internet import reactor, defer 
 27  from twisted.spread import pb, flavors 
 28  from twisted.python import failure, reflect 
 29   
 30  from flumotion.common import errors, interfaces, log, common 
 31  from flumotion.twisted import pb as fpb 
 32   
33 -class ManagerAvatar(fpb.PingableAvatar, log.Loggable):
34 """ 35 I am a base class for manager-side avatars to subclass from. 36 37 @ivar avatarId: the id for this avatar, unique inside the heaven 38 @type avatarId: str 39 @ivar heaven: the heaven this avatar is part of 40 @type heaven: L{flumotion.manager.base.ManagerHeaven} 41 @ivar mind: a remote reference to the client-side Medium 42 @type mind: L{twisted.spread.pb.RemoteReference} 43 @ivar vishnu: the vishnu that manages this avatar's heaven 44 @type vishnu: L{flumotion.manager.manager.Vishnu} 45 """ 46 remoteLogName = 'medium' 47
48 - def __init__(self, heaven, avatarId, remoteIdentity):
49 """ 50 @param heaven: the heaven this avatar is part of 51 @type heaven: L{flumotion.manager.base.ManagerHeaven} 52 @param avatarId: id of the avatar to create 53 @type avatarId: str 54 @param remoteIdentity: manager-assigned identity object for this 55 avatar 56 @type remoteIdentity: L{flumotion.common.identity.RemoteIdentity} 57 """ 58 self.heaven = heaven 59 self.avatarId = avatarId 60 self.logName = avatarId 61 self.mind = None 62 self.vishnu = heaven.vishnu 63 self.remoteIdentity = remoteIdentity 64 self._detachedD = None # deferred to fire when the avatar's mind is 65 # detached 66 67 self.debug("created new Avatar with id %s" % avatarId)
68
69 - def disconnect(self):
70 if self.mind: 71 self.debug("Avatar %r disconnecting", self.avatarId) 72 self._detachedD = defer.Deferred() 73 self.mind.broker.transport.loseConnection() 74 return self._detachedD 75 else: 76 self.debug("Avatar %r is already disconnected", self.avatarId) 77 return defer.succeed(True)
78
79 - def timeoutDisconnect(self):
80 if self.hasRemoteReference(): 81 self.debug("Disconnecting due to ping timeout") 82 self.mind.broker.transport.loseConnection() 83 else: 84 self.debug("Ping timeout, but already disconnected")
85
86 - def hasRemoteReference(self):
87 """ 88 Check if the avatar has a remote reference to the peer. 89 90 @rtype: bool 91 """ 92 return self.mind != None
93 94 # FIXME: we probably need to return Failure objects when something is wrong
95 - def mindCallRemote(self, name, *args, **kwargs):
96 """ 97 Call the given remote method, and log calling and returning nicely. 98 99 @param name: name of the remote method 100 @type name: str 101 """ 102 level = log.DEBUG 103 if name == 'ping': level = log.LOG 104 debugClass = str(self.__class__).split(".")[-1].upper() 105 startArgs = [self.remoteLogName, debugClass, name] 106 format, debugArgs = log.getFormatArgs( 107 '%s --> %s: callRemote(%s, ', startArgs, 108 ')', (), args, kwargs) 109 logKwArgs = self.doLog(level, -2, format, *debugArgs) 110 if not self.hasRemoteReference(): 111 self.warning( 112 "Can't call remote method %s, no mind, except a local Traceback" 113 % name) 114 return 115 116 # we can't do a .debug here, since it will trigger a resend of the 117 # debug message as well, causing infinite recursion ! 118 # self.debug('Calling remote method %s%r' % (name, args)) 119 if not hasattr(self.mind, 'callRemote'): 120 self.error("mind %r does not implement callRemote" % self.mind) 121 return 122 try: 123 d = self.mind.callRemote(name, *args, **kwargs) 124 except pb.DeadReferenceError: 125 self.warning("mind %s is a dead reference, removing" % self.mind) 126 self.mind = None 127 return 128 except Exception, e: 129 self.warning("Exception trying to remote call '%s': %s: %s" % ( 130 name, str(e.__class__), ", ".join(e.args))) 131 return 132 133 def callback(result): 134 format, debugArgs = log.getFormatArgs( 135 '%s <-- %s: callRemote(%s, ', startArgs, 136 '): %r', (log.ellipsize(result), ), args, kwargs) 137 self.doLog(level, -1, format, *debugArgs, **logKwArgs) 138 return result
139 140 def errback(failure): 141 format, debugArgs = log.getFormatArgs( 142 '%s <-- %s: callRemote(%s', startArgs, 143 '): %r', (failure, ), args, kwargs) 144 self.doLog(level, -1, format, *debugArgs, **logKwArgs) 145 return failure
146 147 d.addCallback(callback) 148 d.addErrback(errback) 149 d.addErrback(self._mindCallRemoteErrback, name) 150 # FIXME: is there some way we can register an errback as the 151 # LAST to call as a general fallback ? 152 return d 153
154 - def _mindCallRemoteErrback(self, f, name):
155 if f.check(AttributeError): 156 from twisted.spread import flavors 157 if hasattr(flavors, 'NoSuchMethod'): 158 if f.check(flavors.NoSuchMethod): 159 self.warning("No such remote method '%s'" % name) 160 raise errors.NoMethodError(name) 161 else: 162 raise errors.RemoteRunFailure(name, 163 debug=log.getFailureMessage(f)) 164 165 # revision #13473 of Twisted added NoSuchMethod 166 # this is an older version, so we don't know the difference. 167 self.warning("No such remote method '%s', or AttributeError " 168 "while executing remote method (%s)" % ( 169 name, log.getFailureMessage(f))) 170 raise errors.NoMethodError(name, 171 debug=log.getFailureMessage(f)) 172 173 self.debug("Failure on remote call %s: %r, %s" % (name, 174 f, log.getFailureMessage(f))) 175 return f
176
177 - def attached(self, mind):
178 """ 179 Tell the avatar that the given mind has been attached. 180 This gives the avatar a way to call remotely to the client that 181 requested this avatar. 182 This is scheduled by the portal after the client has logged in. 183 184 @type mind: L{twisted.spread.pb.RemoteReference} 185 """ 186 self.mind = mind 187 transport = self.mind.broker.transport 188 tarzan = transport.getHost() 189 jane = transport.getPeer() 190 if tarzan and jane: 191 self.debug("PB client connection seen by me is from me %s to %s" % ( 192 common.addressGetHost(tarzan), 193 common.addressGetHost(jane))) 194 self.log('Client attached is mind %s' % mind) 195 196 # Now we have a remote reference, so start checking pings. 197 self.startPingChecking(self.timeoutDisconnect)
198
199 - def detached(self, mind):
200 """ 201 Tell the avatar that the peer's client referenced by the mind 202 has detached. 203 204 Called through the manager's PB logout trigger calling 205 L{flumotion.manager.manager.Dispatcher.removeAvatar} 206 207 @type mind: L{twisted.spread.pb.RemoteReference} 208 """ 209 assert(self.mind == mind) 210 self.debug('PB client from %s detached' % self.getClientAddress()) 211 self.stopPingChecking() 212 self.mind = None 213 self.log('Client detached is mind %s' % mind) 214 if self._detachedD: 215 self._detachedD.callback(None) 216 self._detachedD = None
217
218 - def getClientAddress(self):
219 """ 220 Get the IPv4 address of the machine the PB client is connecting from, 221 as seen from the avatar. 222 223 @returns: the IPv4 address the client is coming from, or None. 224 @rtype: str or None 225 """ 226 if self.mind: 227 peer = self.mind.broker.transport.getPeer() 228 # pre-Twisted 1.3.0 compatibility 229 try: 230 return peer.host 231 except AttributeError: 232 return peer[1] 233 234 return None
235
236 - def perspective_getBundleSums(self, bundleName=None, fileName=None, 237 moduleName=None):
238 """ 239 Get a list of (bundleName, md5sum) of all dependency bundles, 240 starting with this bundle, in the correct order. 241 Any of bundleName, fileName, moduleName may be given. 242 243 @type bundleName: str or list of str 244 @param bundleName: the name of the bundle for fetching 245 @type fileName: str or list of str 246 @param fileName: the name of the file requested for fetching 247 @type moduleName: str or list of str 248 @param moduleName: the name of the module requested for import 249 250 @rtype: list of (str, str) tuples of (bundleName, md5sum) 251 """ 252 bundleNames = [] 253 fileNames = [] 254 moduleNames = [] 255 if bundleName: 256 if isinstance(bundleName, str): 257 bundleNames.append(bundleName) 258 else: 259 bundleNames.extend(bundleName) 260 self.debug('asked to get bundle sums for bundles %r' % bundleName) 261 if fileName: 262 if isinstance(fileName, str): 263 fileNames.append(fileName) 264 else: 265 fileNames.extend(fileName) 266 self.debug('asked to get bundle sums for files %r' % fileNames) 267 if moduleName: 268 if isinstance(moduleName, str): 269 moduleNames.append(moduleName) 270 else: 271 moduleNames.extend(moduleName) 272 self.debug('asked to get bundle sums for modules %r' % moduleNames) 273 274 basket = self.vishnu.getBundlerBasket() 275 276 # will raise an error if bundleName not known 277 for fileName in fileNames: 278 bundleName = basket.getBundlerNameByFile(fileName) 279 if not bundleName: 280 msg = 'containing ' + fileName 281 self.warning('No bundle %s' % msg) 282 raise errors.NoBundleError(msg) 283 else: 284 bundleNames.append(bundleName) 285 286 for moduleName in moduleNames: 287 bundleName = basket.getBundlerNameByImport(moduleName) 288 if not bundleName: 289 msg = 'for module ' + moduleName 290 self.warning('No bundle %s' % msg) 291 raise errors.NoBundleError(msg) 292 else: 293 bundleNames.append(bundleName) 294 295 deps = [] 296 for bundleName in bundleNames: 297 thisdeps = basket.getDependencies(bundleName) 298 self.debug('dependencies of %s: %r' % (bundleName, thisdeps[1:])) 299 deps.extend(thisdeps) 300 301 sums = [] 302 for dep in deps: 303 bundler = basket.getBundlerByName(dep) 304 if not bundler: 305 self.warning('Did not find bundle with name %s' % dep) 306 else: 307 sums.append((dep, bundler.bundle().md5sum)) 308 309 self.debug('requested bundles: %r' % [x[0] for x in sums]) 310 return sums
311
312 - def perspective_getBundleSumsByFile(self, filename):
313 """ 314 Get a list of (bundleName, md5sum) of all dependency bundles, 315 starting with this bundle, in the correct order. 316 317 @param filename: the name of the file in a bundle 318 @type filename: str 319 320 @returns: list of (bundleName, md5sum) tuples 321 @rtype: list of (str, str) tuples 322 """ 323 self.debug('asked to get bundle sums for file %s' % filename) 324 basket = self.vishnu.getBundlerBasket() 325 bundleName = basket.getBundlerNameByFile(filename) 326 if not bundleName: 327 self.warning('Did not find a bundle for file %s' % filename) 328 raise errors.NoBundleError("for file %s" % filename) 329 330 return self.perspective_getBundleSums(bundleName)
331
332 - def perspective_getBundleZips(self, bundles):
333 """ 334 Get the zip files for the given list of bundles. 335 336 @param bundles: the names of the bundles to get 337 @type bundles: list of str 338 339 @returns: dictionary of bundleName -> zipdata 340 @rtype: dict of str -> str 341 """ 342 basket = self.vishnu.getBundlerBasket() 343 zips = {} 344 for name in bundles: 345 bundler = basket.getBundlerByName(name) 346 if not bundler: 347 raise errors.NoBundleError('The bundle named "%s" was not found' 348 % (name,)) 349 zips[name] = bundler.bundle().getZip() 350 return zips
351
352 - def perspective_authenticate(self, bouncerName, keycard):
353 """ 354 Authenticate the given keycard. 355 If no bouncerName given, authenticate against the manager's bouncer. 356 If a bouncerName is given, authenticate against the given bouncer 357 in the atmosphere. 358 359 @since: 0.3.1 360 361 @param bouncerName: the name of the atmosphere bouncer, or None 362 @type bouncerName: str or None 363 @param keycard: the keycard to authenticate 364 @type keycard: L{flumotion.common.keycards.Keycard} 365 366 @returns: a deferred, returning the keycard or None. 367 """ 368 if not bouncerName: 369 self.debug( 370 'asked to authenticate keycard %r using manager bouncer' % 371 keycard) 372 return self.vishnu.bouncer.authenticate(keycard) 373 374 self.debug('asked to authenticate keycard %r using bouncer %s' % ( 375 keycard, bouncerName)) 376 avatarId = common.componentId('atmosphere', bouncerName) 377 if not self.heaven.hasAvatar(avatarId): 378 self.warning('No bouncer with id %s registered' % avatarId) 379 raise errors.UnknownComponentError(avatarId) 380 381 bouncerAvatar = self.heaven.getAvatar(avatarId) 382 return bouncerAvatar.authenticate(keycard)
383
384 - def perspective_getKeycardClasses(self):
385 """ 386 Get the keycard classes the manager's bouncer can authenticate. 387 388 @since: 0.3.1 389 390 @returns: a deferred, returning a list of keycard class names 391 @rtype: L{twisted.internet.defer.Deferred} firing list of str 392 """ 393 list = self.vishnu.bouncer.keycardClasses 394 return [reflect.qual(c) for c in list]
395
396 -class ManagerHeaven(pb.Root, log.Loggable):
397 """ 398 I am a base class for heavens in the manager. 399 400 @cvar avatarClass: the class object this heaven instantiates avatars from. 401 To be set in subclass. 402 @ivar avatars: a dict of avatarId -> Avatar 403 @type avatars: dict of str -> L{ManagerAvatar} 404 @ivar vishnu: the Vishnu in control of all the heavens 405 @type vishnu: L{flumotion.manager.manager.Vishnu} 406 """ 407 avatarClass = None 408
409 - def __init__(self, vishnu):
410 """ 411 @param vishnu: the Vishnu in control of all the heavens 412 @type vishnu: L{flumotion.manager.manager.Vishnu} 413 """ 414 self.vishnu = vishnu 415 self.avatars = {} # avatarId -> avatar
416 417 ### ManagerHeaven methods
418 - def createAvatar(self, avatarId, remoteIdentity):
419 """ 420 Create a new avatar and manage it. 421 422 @param avatarId: id of the avatar to create 423 @type avatarId: str 424 @param remoteIdentity: the manager-side representation of the 425 remote identity 426 @type remoteIdentity: L{flumotion.common.identity.RemoteIdentity} 427 428 @returns: a new avatar for the client 429 @rtype: L{flumotion.manager.base.ManagerAvatar} 430 """ 431 self.debug('creating new Avatar with name %s' % avatarId) 432 if self.avatars.has_key(avatarId): 433 self.warning('an avatar named %s is already logged in' % 434 avatarId) 435 raise errors.AlreadyConnectedError(avatarId) 436 437 avatar = self.avatarClass(self, avatarId, remoteIdentity) 438 439 self.avatars[avatarId] = avatar 440 return avatar
441
442 - def removeAvatar(self, avatarId):
443 """ 444 Stop managing the given avatar. 445 446 @param avatarId: id of the avatar to remove 447 @type avatarId: str 448 """ 449 self.debug('removing Avatar with id %s' % avatarId) 450 del self.avatars[avatarId]
451
452 - def getAvatar(self, avatarId):
453 """ 454 Get the avatar with the given id. 455 456 @param avatarId: id of the avatar to get 457 @type avatarId: str 458 459 @returns: the avatar with the given id 460 @rtype: L{ManagerAvatar} 461 """ 462 return self.avatars[avatarId]
463
464 - def hasAvatar(self, avatarId):
465 """ 466 Check if a component with that name is registered. 467 468 @param avatarId: id of the avatar to check 469 @type avatarId: str 470 471 @returns: True if an avatar with that id is registered 472 @rtype: bool 473 """ 474 return self.avatars.has_key(avatarId)
475
476 - def getAvatars(self):
477 """ 478 Get all avatars in this heaven. 479 480 @returns: a list of all avatars in this heaven 481 @rtype: list of L{ManagerAvatar} 482 """ 483 return self.avatars.values()
484