1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
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
65
66
67 self.debug("created new Avatar with id %s" % avatarId)
68
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
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
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
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
117
118
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
151
152 return d
153
176
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
197 self.startPingChecking(self.timeoutDisconnect)
198
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
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
229 try:
230 return peer.host
231 except AttributeError:
232 return peer[1]
233
234 return None
235
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
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
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
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
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
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
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
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 = {}
416
417
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
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
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
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
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