1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """
23 model abstraction for administration clients supporting different views
24 """
25
26 from twisted.internet import error, defer, reactor
27 from zope.interface import implements
28
29 from flumotion.common import common, errors, interfaces, log
30 from flumotion.common import medium
31 from flumotion.common import messages, signals
32 from flumotion.common import planet, worker
33 from flumotion.common.i18n import N_, gettexter
34 from flumotion.configure import configure
35 from flumotion.twisted import pb as fpb
36
37 __version__ = "$Rev$"
38 T_ = gettexter()
39
40
42 perspectiveInterface = interfaces.IAdminMedium
43
44 - def __init__(self, medium, extraTenacious=False, maxDelay=20):
57
62
67
69 """
70 @type connector: implementation of
71 L{twisted.internet.interfaces.IConnector}
72 @param reason: L{twisted.spread.pb.failure.Failure}
73 """
74 self.debug("Lost connection to %s: %s",
75 connector.getDestination(), log.getFailureMessage(reason))
76 if self.hasBeenAuthenticated:
77 self.log("Have been authenticated before. Trying again.")
78 elif self.extraTenacious:
79 self.log("We are extra tenacious, trying again")
80 else:
81 self.log("Telling medium about connection failure")
82 self.medium.connectionFailed(reason)
83 return
84
85 RFC = fpb.ReconnectingFPBClientFactory
86 RFC.clientConnectionLost(self, connector, reason)
87
89 """
90 @type connector: implementation of
91 L{twisted.internet.interfaces.IConnector}
92 @param reason: L{twisted.spread.pb.failure.Failure}
93 """
94 if reason.check(error.DNSLookupError):
95 self.debug('DNS lookup error')
96 if not self.extraTenacious:
97 self.medium.connectionFailed(reason)
98 return
99 elif (reason.check(error.ConnectionRefusedError)
100 or reason.check(error.ConnectError)):
101
102
103
104
105
106 self.debug("Error connecting to %s: %s",
107 connector.getDestination(),
108 log.getFailureMessage(reason))
109 if self.hasBeenConnected:
110 self.log("we've been connected before though, so going "
111 "to retry")
112
113 elif self.extraTenacious:
114 self.log("trying again due to +100 tenacity")
115
116 else:
117 self.log("telling medium about connection failure")
118 self.medium.connectionFailed(reason)
119 return
120
121 fpb.ReconnectingFPBClientFactory.clientConnectionFailed(self,
122 connector, reason)
123
124
125
131
132 def error(failure):
133 if self.extraTenacious:
134 self.debug('connection problem to %s: %s',
135 self._connector.getDestination(),
136 log.getFailureMessage(failure))
137 self.debug('we are tenacious, so trying again later')
138 self.disconnect()
139 elif failure.check(errors.ConnectionFailedError):
140 self.debug("emitting connection-failed")
141 self.medium.emit('connection-failed', "I failed my master")
142 self.debug("emitted connection-failed")
143 elif failure.check(errors.ConnectionRefusedError):
144 self.debug("emitting connection-refused")
145 self.medium.emit('connection-refused')
146 self.debug("emitted connection-refused")
147 elif failure.check(errors.NotAuthenticatedError):
148
149 self.debug("emitting connection-refused")
150 self.medium.emit('connection-refused')
151 self.debug("emitted connection-refused")
152 else:
153 self.medium.emit('connection-error', failure)
154 self.warning('connection error to %s:: %s',
155 self._connector.getDestination(),
156 log.getFailureMessage(failure))
157
158
159 d.addCallbacks(success, error)
160 return d
161
162
163
164
165
166 -class AdminModel(medium.PingingMedium, signals.SignalMixin):
167 """
168 I live in the admin client.
169 I am a data model for any admin view implementing a UI to
170 communicate with one manager.
171 I send signals when things happen.
172
173 Manager calls on us through L{flumotion.manager.admin.AdminAvatar}
174 """
175 __signals__ = ('connected', 'disconnected', 'connection-refused',
176 'connection-failed', 'connection-error', 'reloading',
177 'message', 'update')
178
179 logCategory = 'adminmodel'
180
181 implements(interfaces.IAdminMedium)
182
183
184 planet = None
185
187
188 self.connectionInfo = None
189 self.keepTrying = None
190 self._writeConnection = True
191
192 self.managerId = '<uninitialized>'
193
194 self.connected = False
195 self.clientFactory = None
196
197 self._deferredConnect = None
198
199 self._components = {}
200 self.planet = None
201 self._workerHeavenState = None
202
204 """
205 Disconnects from the actual manager and frees the connection.
206 """
207 if self.clientFactory:
208
209
210 if self.remote:
211 self.remote.dontNotifyOnDisconnect(self._remoteDisconnected)
212
213 self.clientFactory.stopTrying()
214
215 self.clientFactory.disconnect()
216 self.clientFactory = None
217
218 - def connectToManager(self, connectionInfo, keepTrying=False,
219 writeConnection=True):
220 """
221 Connects to the specified manager.
222
223 @param connectionInfo: data for establishing the connection
224 @type connectionInfo: a L{PBConnectionInfo}
225 @param keepTrying: when this is L{True} the Factory will try to
226 reconnect when it loses the connection
227 @type keepTrying: bool
228 @param writeConnection: when this is L{True} the connection is saved
229 for future uses on cache
230 @type writeConnection: bool
231
232 @rtype: L{twisted.internet.defer.Deferred}
233 """
234 assert self.clientFactory is None
235
236 self.connectionInfo = connectionInfo
237 self._writeConnection = writeConnection
238
239
240
241
242 self.managerId = str(connectionInfo)
243 self.logName = self.managerId
244
245 self.info('Connecting to manager %s with %s',
246 self.managerId, connectionInfo.use_ssl and 'SSL' or 'TCP')
247
248 self.clientFactory = AdminClientFactory(self,
249 extraTenacious=keepTrying,
250 maxDelay=20)
251 self.clientFactory.startLogin(connectionInfo.authenticator)
252
253 if connectionInfo.use_ssl:
254 common.assertSSLAvailable()
255 from twisted.internet import ssl
256 reactor.connectSSL(connectionInfo.host, connectionInfo.port,
257 self.clientFactory, ssl.ClientContextFactory())
258 else:
259 reactor.connectTCP(connectionInfo.host, connectionInfo.port,
260 self.clientFactory)
261
262 def connected(model, d):
263
264 d.callback(model)
265
266 def disconnected(model, d):
267
268
269 if not keepTrying:
270 d.errback(errors.ConnectionFailedError('Lost connection'))
271
272 def connection_refused(model, d):
273 if not keepTrying:
274 d.errback(errors.ConnectionRefusedError())
275
276 def connection_failed(model, reason, d):
277 if not keepTrying:
278 d.errback(errors.ConnectionFailedError(reason))
279
280 def connection_error(model, failure, d):
281 if not keepTrying:
282 d.errback(failure)
283
284 d = defer.Deferred()
285 ids = []
286 ids.append(self.connect('connected', connected, d))
287 ids.append(self.connect('disconnected', disconnected, d))
288 ids.append(self.connect('connection-refused', connection_refused, d))
289 ids.append(self.connect('connection-failed', connection_failed, d))
290 ids.append(self.connect('connection-error', connection_error, d))
291
292 def success(model):
293 map(self.disconnect, ids)
294 self._deferredConnect = None
295 return model
296
297 def failure(f):
298 map(self.disconnect, ids)
299 self._deferredConnect = None
300 return f
301
302 d.addCallbacks(success, failure)
303 self._deferredConnect = d
304 return d
305
307 """
308 Handle all coding mistakes that could be triggered by loading bundles.
309 This is a convenience method to help in properly reporting problems.
310 The EntrySyntaxError should be caught and wrapped in a UI message,
311 with the message generated here as debug information.
312
313 @param failure: the failure to be handled
314 @type failure: L{twisted.python.failure.Failure}
315 @param filename: name of the file being loaded
316 @type filename: str
317
318 @raises: L{errors.EntrySyntaxError}
319 """
320 try:
321 raise failure.value
322 except SyntaxError, e:
323
324 where = getattr(e, 'filename', "<entry file>")
325 lineno = getattr(e, 'lineno', 0)
326 msg = "Syntax Error at %s:%d while executing %s" % (
327 where, lineno, fileName)
328 self.warning(msg)
329 raise errors.EntrySyntaxError(msg)
330 except NameError, e:
331 msg = "NameError while executing %s: %s" % (
332 fileName, " ".join(e.args))
333 self.warning(msg)
334 raise errors.EntrySyntaxError(msg)
335 except ImportError, e:
336 msg = "ImportError while executing %s: %s" % (fileName,
337 " ".join(e.args))
338 self.warning(msg)
339 raise errors.EntrySyntaxError(msg)
340
342 self.debug('shutting down')
343 if self.clientFactory is not None:
344
345
346 self.clientFactory.stopTrying()
347 self.clientFactory.disconnect()
348 self.clientFactory = None
349
350 if self._deferredConnect is not None:
351
352 self.debug('cancelling connection attempt')
353 self._deferredConnect.errback(errors.ConnectionCancelledError())
354
356 """Close any existing connection to the manager and
357 reconnect."""
358 self.debug('asked to log in again')
359 self.shutdown()
360 return self.connectToManager(self.connectionInfo, keepTrying)
361
362
363
365 return self.managerId
366
368 return '%s:%s (%s)' % (self.connectionInfo.host,
369 self.connectionInfo.port,
370 self.connectionInfo.use_ssl
371 and 'https' or 'http')
372
373
374
376 assert self.planet
377 return '%s (%s)' % (self.planet.get('name'), self.managerId)
378
394
396 self.debug("setRemoteReference %r", remoteReference)
397
398 def gotPlanetState(planet):
399 self.planet = planet
400
401 self.planet.admin = self
402 self.debug('got planet state')
403 return self.callRemote('getWorkerHeavenState')
404
405 def gotWorkerHeavenState(whs):
406 self._workerHeavenState = whs
407 self.debug('got worker state')
408
409 self.debug('Connected to manager and retrieved all state')
410 self.connected = True
411 if self._writeConnection:
412 writeConnection()
413 self.emit('connected')
414
415 def writeConnection():
416 i = self.connectionInfo
417 if not (i.authenticator.username
418 and i.authenticator.password):
419 self.log('not caching connection information')
420 return
421 s = ''.join(['<connection>',
422 '<host>%s</host>' % i.host,
423 '<manager>%s</manager>' % self.planet.get('name'),
424 '<port>%d</port>' % i.port,
425 '<use_insecure>%d</use_insecure>'
426 % ((not i.use_ssl) and 1 or 0),
427 '<user>%s</user>' % i.authenticator.username,
428 '<passwd>%s</passwd>' % i.authenticator.password,
429 '</connection>'])
430
431 import os
432 from flumotion.common import python
433 md5sum = python.md5(s).hexdigest()
434 f = os.path.join(configure.registrydir, '%s.connection' % md5sum)
435 try:
436 h = open(f, 'w')
437 h.write(s)
438 h.close()
439 except Exception, e:
440 self.info('failed to write connection cache file %s: %s',
441 f, log.getExceptionMessage(e))
442
443
444 medium.PingingMedium.setRemoteReference(self, remoteReference)
445
446
447
448 self.remote.notifyOnDisconnect(self._remoteDisconnected)
449
450 d = self.callRemote('getPlanetState')
451 d.addCallback(gotPlanetState)
452 d.addCallback(gotWorkerHeavenState)
453 return d
454
455
456
457
458
461
462
463
465 """
466 Call the given method on the given component with the given args.
467
468 @param componentState: component to call the method on
469 @type componentState: L{flumotion.common.planet.AdminComponentState}
470 @param methodName: name of method to call; serialized to a
471 remote_methodName on the worker's medium
472
473 @rtype: L{twisted.internet.defer.Deferred}
474 """
475 d = self.callRemote('componentCallRemote',
476 componentState, methodName,
477 *args, **kwargs)
478
479 def errback(failure):
480 msg = None
481 if failure.check(errors.NoMethodError):
482 msg = "Remote method '%s' does not exist." % methodName
483 msg += "\n" + failure.value
484 else:
485 msg = log.getFailureMessage(failure)
486
487
488
489
490 self.warning(msg)
491 m = messages.Warning(T_(N_("Internal error in component.")),
492 debug=msg)
493 componentState.observe_append('messages', m)
494 return failure
495
496 d.addErrback(errback)
497
498 return d
499
501 """
502 Call the the given method on the given worker with the given args.
503
504 @param workerName: name of the worker to call the method on
505 @param methodName: name of method to call; serialized to a
506 remote_methodName on the worker's medium
507
508 @rtype: L{twisted.internet.defer.Deferred}
509 """
510 return self.callRemote('workerCallRemote', workerName,
511 methodName, *args, **kwargs)
512
513
514
516 return self.callRemote('loadConfiguration', xml_string)
517
520
522 """
523 Obtains the available scenarios from the manager.
524
525 @rtype: L{twisted.internet.defer.Deferred}
526 """
527 return self.callRemote('getScenarios')
528
530 """
531 Obtains an scenario given its type.
532
533 @rtype: L{twisted.internet.defer.Deferred}
534 """
535 return self.callRemote('getScenarioByType', type)
536
539
540
541
544
547
548 - def workerRun(self, workerName, moduleName, functionName, *args, **kwargs):
549 """
550 Run the given function and args on the given worker. If the
551 worker does not already have the module, or it is out of date,
552 it will be retrieved from the manager.
553
554 @rtype: L{twisted.internet.defer.Deferred} firing an
555 L{flumotion.common.messages.Result}
556 """
557 return self.workerCallRemote(workerName, 'runFunction', moduleName,
558 functionName, *args, **kwargs)
559
561 return self.callRemote('getWizardEntries',
562 wizardTypes, provides, accepts)
563
565 return self._workerHeavenState
566
568 self.debug("emitting disconnected")
569 self.connected = False
570 self.emit('disconnected')
571 self.debug("emitted disconnected")
572