Package flumotion :: Package wizard :: Module configurationwizard
[hide private]

Source Code for Module flumotion.wizard.configurationwizard

  1  # -*- Mode: Python -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007,2008 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  import gettext 
 23  import os 
 24  import sets 
 25   
 26  import gtk 
 27  from gtk import gdk 
 28  from twisted.internet import defer 
 29   
 30  from flumotion.common import errors, messages 
 31  from flumotion.common.common import pathToModuleName 
 32  from flumotion.common.i18n import N_, ngettext, gettexter 
 33  from flumotion.common.pygobject import gsignal 
 34  from flumotion.ui.wizard import SectionWizard, WizardStep 
 35  from flumotion.wizard.scenarios import LiveScenario, OnDemandScenario 
 36  from flumotion.wizard.worker import WorkerList 
 37  from flumotion.wizard.workerstep import WorkerWizardStep 
 38   
 39   
 40  # pychecker doesn't like the auto-generated widget attrs 
 41  # or the extra args we name in callbacks 
 42  __pychecker__ = 'no-classattr no-argsused' 
 43  __version__ = "$Rev: 7088 $" 
 44  T_ = gettexter() 
 45  _ = gettext.gettext 
 46   
 47   
 48  # the denominator arg for all calls of this function was sniffed from 
 49  # the glade file's spinbutton adjustment 
 50   
51 -def _fraction_from_float(number, denominator):
52 """ 53 Return a string to be used in serializing to XML. 54 """ 55 return "%d/%d" % (number * denominator, denominator)
56 57
58 -class WelcomeStep(WizardStep):
59 name = "Welcome" 60 title = _('Welcome') 61 section = _('Welcome') 62 icon = 'wizard.png' 63 gladeFile = 'welcome-wizard.glade' 64
65 - def getNext(self):
66 return None
67 68
69 -class ScenarioStep(WizardStep):
70 name = "Scenario" 71 title = _('Scenario') 72 section = _('Scenario') 73 icon = 'wizard.png' 74 gladeFile = 'scenario-wizard.glade' 75 76 # WizardStep
77 - def __init__(self, wizard):
78 self._currentScenarioClass = None 79 self._radioGroup = None 80 self._scenarioRadioButtons = [] 81 super(ScenarioStep, self).__init__(wizard)
82
83 - def setup(self):
84 self.addScenario(LiveScenario) 85 self.addScenario(OnDemandScenario) 86 firstButton = self.scenarios_box.get_children()[0] 87 firstButton.set_active(True) 88 firstButton.toggled() 89 firstButton.grab_focus()
90
91 - def getNext(self):
92 self.wizard.cleanFutureSteps() 93 scenario = self._currentScenarioClass(self.wizard) 94 scenario.addSteps() 95 self.wizard.setScenario(scenario)
96 97 # Public 98
99 - def addScenario(self, scenarioClass):
100 button = gtk.RadioButton(self._radioGroup, scenarioClass.short) 101 button.connect('toggled', self._on_radiobutton__toggled, scenarioClass) 102 button.connect('activate', self._on_radiobutton__activate) 103 104 self.scenarios_box.pack_start(button, False, False) 105 button.show() 106 107 if self._radioGroup is None: 108 self._radioGroup = button
109 110 # Private 111 112 # Callbacks 113
114 - def _on_radiobutton__activate(self, radio):
115 self.wizard.goNext()
116
117 - def _on_radiobutton__toggled(self, radio, scenarioClass):
118 if radio.get_active(): 119 self._currentScenarioClass = scenarioClass
120 121
122 -class ConfigurationWizard(SectionWizard):
123 gsignal('finished', str) 124
125 - def __init__(self, parent=None):
126 SectionWizard.__init__(self, parent) 127 # Set the name of the toplevel window, this is used by the 128 # ui unittest framework 129 self.window1.set_name('ConfigurationWizard') 130 self._cursorWatch = gdk.Cursor(gdk.WATCH) 131 self._tasks = [] 132 self._adminModel = None 133 self._workerHeavenState = None 134 self._lastWorker = 0 # combo id last worker from step to step 135 self._httpPorter = None 136 self._stepWorkers = {} 137 self._scenario = None 138 self._existingComponentNames = [] 139 140 self._workerList = WorkerList() 141 self.top_vbox.pack_start(self._workerList, False, False) 142 self._workerList.connect('worker-selected', 143 self.on_combobox_worker_changed) 144 self.addSteps()
145 146 # SectionWizard 147
148 - def completed(self):
149 saver = self._scenario.save() 150 xml = saver.getXML() 151 self.emit('finished', xml)
152
153 - def destroy(self):
154 SectionWizard.destroy(self) 155 self._adminModel = None
156
157 - def beforeShowStep(self, step):
158 if isinstance(step, WorkerWizardStep): 159 self._workerList.show() 160 self._workerList.notifySelected() 161 else: 162 self._workerList.hide() 163 164 self._setupWorker(step, self._workerList.getWorker())
165
166 - def prepareNextStep(self, step):
167 self._setupWorker(step, self._workerList.getWorker()) 168 SectionWizard.prepareNextStep(self, step)
169
170 - def blockNext(self, block):
171 # Do not block/unblock if we have tasks running 172 if self._tasks: 173 return 174 SectionWizard.blockNext(self, block)
175 176 # Public API 177 178 # FIXME: Remove this and make fgc create a new scenario
179 - def addSteps(self):
180 """Add the step sections of the wizard, can be 181 overridden in a subclass 182 """ 183 # These two steps are independent of the scenarios, they 184 # should always be added. 185 self.addStepSection(WelcomeStep) 186 self.addStepSection(ScenarioStep)
187
188 - def setScenario(self, scenario):
189 """Sets the current scenario of the wizard. 190 Normally called by ScenarioStep to tell the wizard the current scenario 191 just after creating it. 192 @param scenario: the scenario of the wizard 193 @type scenario: a L{flumotion.wizard.scenarios.Scenario} subclass 194 """ 195 scenario.setExistingComponentNames(self._existingComponentNames) 196 self._scenario = scenario
197
198 - def getScenario(self):
199 """Fetches the currently set scenario of the wizard. 200 @returns scenario: the scenario of the wizard 201 @rtype: a L{flumotion.wizard.scenarios.Scenario} subclass 202 """ 203 return self._scenario
204
205 - def setWorkerHeavenState(self, workerHeavenState):
206 """ 207 Sets the worker heaven state of the wizard 208 @param workerHeavenState: the worker heaven state 209 @type workerHeavenState: L{WorkerComponentUIState} 210 """ 211 self._workerHeavenState = workerHeavenState 212 self._workerList.setWorkerHeavenState(workerHeavenState)
213
214 - def setAdminModel(self, adminModel):
215 """ 216 Sets the admin model of the wizard 217 @param adminModel: the admin model 218 @type adminModel: L{AdminModel} 219 """ 220 self._adminModel = adminModel
221
222 - def getAdminModel(self):
223 """Gets the admin model of the wizard 224 @returns adminModel: the admin model 225 @rtype adminModel: L{AdminModel} 226 """ 227 return self._adminModel
228
229 - def waitForTask(self, taskName):
230 """Instruct the wizard that we're waiting for a task 231 to be finished. This changes the cursor and prevents 232 the user from continuing moving forward. 233 Each call to this method should have another call 234 to taskFinished() when the task is actually done. 235 @param taskName: name of the name 236 @type taskName: string 237 """ 238 self.info("waiting for task %s" % (taskName,)) 239 if not self._tasks: 240 if self.window1.window is not None: 241 self.window1.window.set_cursor(self._cursorWatch) 242 self.blockNext(True) 243 self._tasks.append(taskName)
244
245 - def taskFinished(self, blockNext=False):
246 """Instruct the wizard that a task was finished. 247 @param blockNext: if we should still next when done 248 @type blockNext: boolean 249 """ 250 if not self._tasks: 251 raise AssertionError( 252 "Stray call to taskFinished(), forgot to call waitForTask()?") 253 254 taskName = self._tasks.pop() 255 self.info("task %s has now finished" % (taskName,)) 256 if not self._tasks: 257 self.window1.window.set_cursor(None) 258 self.blockNext(blockNext)
259
260 - def pendingTask(self):
261 """Returns true if there are any pending tasks 262 @returns: if there are pending tasks 263 @rtype: bool 264 """ 265 return bool(self._tasks)
266 267 # FIXME: Move to scenario
268 - def hasAudio(self):
269 """If the configured feed has a audio stream 270 @return: if we have audio 271 @rtype: bool 272 """ 273 productionStep = self.getStep('Production') 274 return productionStep.hasAudio()
275
276 - def hasVideo(self):
277 """If the configured feed has a video stream 278 @return: if we have video 279 @rtype: bool 280 """ 281 productionStep = self.getStep('Production') 282 return productionStep.hasVideo()
283
284 - def getAudioProducer(self):
285 """Returns the selected audio producer or None 286 @returns: producer or None 287 @rtype: L{flumotion.wizard.models.AudioProducer} 288 """ 289 productionStep = self.getStep('Production') 290 return productionStep.getAudioProducer()
291
292 - def getVideoProducer(self):
293 """Returns the selected video producer or None 294 @returns: producer or None 295 @rtype: L{flumotion.wizard.models.VideoProducer} 296 """ 297 productionStep = self.getStep('Production') 298 return productionStep.getVideoProducer()
299
300 - def setHTTPPorter(self, httpPorter):
301 """Sets the HTTP porter of the wizard. 302 If the http port set in the new wizard is identical to the old one, 303 this porter will be reused 304 @param httpPorter: the http porter 305 @type httpPorter: L{flumotion.wizard.models.Porter} instance 306 """ 307 self._httpPorter = httpPorter
308
309 - def getHTTPPorter(self):
310 return self._httpPorter
311
312 - def checkElements(self, workerName, *elementNames):
313 """Check if the given list of GStreamer elements exist on the 314 given worker. 315 @param workerName: name of the worker to check on 316 @type workerName: string 317 @param elementNames: names of the elements to check 318 @type elementNames: list of strings 319 @returns: a deferred returning a tuple of the missing elements 320 @rtype: L{twisted.internet.defer.Deferred} 321 """ 322 if not self._adminModel: 323 self.debug('No admin connected, not checking presence of elements') 324 return 325 326 asked = sets.Set(elementNames) 327 def _checkElementsCallback(existing, workerName): 328 existing = sets.Set(existing) 329 self.taskFinished() 330 return tuple(asked.difference(existing))
331 332 self.waitForTask('check elements %r' % (elementNames,)) 333 d = self._adminModel.checkElements(workerName, elementNames) 334 d.addCallback(_checkElementsCallback, workerName) 335 return d
336
337 - def requireElements(self, workerName, *elementNames):
338 """Require that the given list of GStreamer elements exists on the 339 given worker. If the elements do not exist, an error message is 340 posted and the next button remains blocked. 341 @param workerName: name of the worker to check on 342 @type workerName: string 343 @param elementNames: names of the elements to check 344 @type elementNames: list of strings 345 @returns: element name 346 @rtype: deferred -> list of strings 347 """ 348 if not self._adminModel: 349 self.debug('No admin connected, not checking presence of elements') 350 return 351 352 self.debug('requiring elements %r' % (elementNames,)) 353 def gotMissingElements(elements, workerName): 354 if elements: 355 self.warning('elements %r do not exist' % (elements,)) 356 f = ngettext("Worker '%s' is missing GStreamer element '%s'.", 357 "Worker '%s' is missing GStreamer elements '%s'.", 358 len(elements)) 359 message = messages.Error(T_(f, workerName, 360 "', '".join(elements))) 361 message.add(T_(N_("\n" 362 "Please install the necessary GStreamer plug-ins that " 363 "provide these elements and restart the worker."))) 364 message.add(T_(N_("\n\n" 365 "You will not be able to go forward using this worker."))) 366 message.id = 'element' + '-'.join(elementNames) 367 self.add_msg(message) 368 self.taskFinished(bool(elements)) 369 return elements
370 371 self.waitForTask('require elements %r' % (elementNames,)) 372 d = self.checkElements(workerName, *elementNames) 373 d.addCallback(gotMissingElements, workerName) 374 375 return d 376
377 - def checkImport(self, workerName, moduleName):
378 """Check if the given module can be imported. 379 @param workerName: name of the worker to check on 380 @type workerName: string 381 @param moduleName: name of the module to import 382 @type moduleName: string 383 @returns: a deferred firing None or Failure. 384 @rtype: L{twisted.internet.defer.Deferred} 385 """ 386 if not self._adminModel: 387 self.debug('No admin connected, not checking presence of elements') 388 return 389 390 d = self._adminModel.checkImport(workerName, moduleName) 391 return d
392
393 - def requireImport(self, workerName, moduleName, projectName=None, 394 projectURL=None):
395 """Require that the given module can be imported on the given worker. 396 If the module cannot be imported, an error message is 397 posted and the next button remains blocked. 398 @param workerName: name of the worker to check on 399 @type workerName: string 400 @param moduleName: name of the module to import 401 @type moduleName: string 402 @param projectName: name of the module to import 403 @type projectName: string 404 @param projectURL: URL of the project 405 @type projectURL: string 406 @returns: a deferred firing None or Failure 407 @rtype: L{twisted.internet.defer.Deferred} 408 """ 409 if not self._adminModel: 410 self.debug('No admin connected, not checking presence of elements') 411 return 412 413 self.debug('requiring module %s' % moduleName) 414 def _checkImportErrback(failure): 415 self.warning('could not import %s', moduleName) 416 message = messages.Error(T_(N_( 417 "Worker '%s' cannot import module '%s'."), 418 workerName, moduleName)) 419 if projectName: 420 message.add(T_(N_("\n" 421 "This module is part of '%s'."), projectName)) 422 if projectURL: 423 message.add(T_(N_("\n" 424 "The project's homepage is %s"), projectURL)) 425 message.add(T_(N_("\n\n" 426 "You will not be able to go forward using this worker."))) 427 message.id = 'module-%s' % moduleName 428 self.add_msg(message) 429 self.taskFinished(True)
430 431 d = self.checkImport(workerName, moduleName) 432 d.addErrback(_checkImportErrback) 433 return d 434 435 # FIXME: maybe add id here for return messages ?
436 - def runInWorker(self, workerName, moduleName, functionName, 437 *args, **kwargs):
438 """ 439 Run the given function and arguments on the selected worker. 440 The given function should return a L{messages.Result}. 441 442 @param workerName: name of the worker to run the function in 443 @type workerName: string 444 @param moduleName: name of the module where the function is found 445 @type moduleName: string 446 @param functionName: name of the function to run 447 @type functionName: string 448 449 @returns: a deferred firing the Result's value. 450 @rtype: L{twisted.internet.defer.Deferred} 451 """ 452 self.debug('runInWorker(moduleName=%r, functionName=%r)' % ( 453 moduleName, functionName)) 454 admin = self._adminModel 455 if not admin: 456 self.warning('skipping runInWorker, no admin') 457 return defer.fail(errors.FlumotionError('no admin')) 458 459 if not workerName: 460 self.warning('skipping runInWorker, no worker') 461 return defer.fail(errors.FlumotionError('no worker')) 462 463 def callback(result): 464 self.debug('runInWorker callbacked a result') 465 self.clear_msg(functionName) 466 467 if not isinstance(result, messages.Result): 468 msg = messages.Error(T_( 469 N_("Internal error: could not run check code on worker.")), 470 debug=('function %r returned a non-Result %r' 471 % (functionName, result))) 472 self.add_msg(msg) 473 self.taskFinished(True) 474 raise errors.RemoteRunError(functionName, 'Internal error.') 475 476 for m in result.messages: 477 self.debug('showing msg %r' % m) 478 self.add_msg(m) 479 480 if result.failed: 481 self.debug('... that failed') 482 self.taskFinished(True) 483 raise errors.RemoteRunFailure(functionName, 'Result failed') 484 self.debug('... that succeeded') 485 self.taskFinished() 486 return result.value
487 488 def errback(failure): 489 self.debug('runInWorker errbacked, showing error msg') 490 if failure.check(errors.RemoteRunError): 491 debug = failure.value 492 else: 493 debug = "Failure while running %s.%s:\n%s" % ( 494 moduleName, functionName, failure.getTraceback()) 495 496 msg = messages.Error(T_( 497 N_("Internal error: could not run check code on worker.")), 498 debug=debug) 499 self.add_msg(msg) 500 self.taskFinished(True) 501 raise errors.RemoteRunError(functionName, 'Internal error.') 502 503 self.waitForTask('run in worker: %s.%s(%r, %r)' % ( 504 moduleName, functionName, args, kwargs)) 505 d = admin.workerRun(workerName, moduleName, 506 functionName, *args, **kwargs) 507 d.addErrback(errback) 508 d.addCallback(callback) 509 return d 510
511 - def getWizardEntry(self, componentType):
512 """Fetches a wizard bundle from a specific kind of component 513 @param componentType: the component type to get the wizard entry 514 bundle from. 515 @type componentType: string 516 @returns: a deferred returning either:: 517 - factory of the component 518 - noBundle error: if the component lacks a wizard bundle 519 @rtype: L{twisted.internet.defer.Deferred} 520 """ 521 self.waitForTask('get wizard entry %s' % (componentType,)) 522 self.clear_msg('wizard-bundle') 523 d = self._adminModel.callRemote( 524 'getEntryByType', componentType, 'wizard') 525 d.addCallback(self._gotEntryPoint) 526 return d
527
528 - def getWizardPlugEntry(self, plugType):
529 """Fetches a wizard bundle from a specific kind of plug 530 @param plugType: the plug type to get the wizard entry 531 bundle from. 532 @type plugType: string 533 @returns: a deferred returning either:: 534 - factory of the plug 535 - noBundle error: if the plug lacks a wizard bundle 536 @rtype: L{twisted.internet.defer.Deferred} 537 """ 538 self.waitForTask('get wizard plug %s' % (plugType,)) 539 self.clear_msg('wizard-bundle') 540 d = self._adminModel.callRemote( 541 'getPlugEntry', plugType, 'wizard') 542 d.addCallback(self._gotEntryPoint) 543 return d
544
545 - def getWizardEntries(self, wizardTypes=None, provides=None, accepts=None):
546 """Queries the manager for a list of wizard entries matching the 547 query. 548 @param wizardTypes: list of component types to fetch, is usually 549 something like ['video-producer'] or 550 ['audio-encoder'] 551 @type wizardTypes: list of str 552 @param provides: formats provided, eg ['jpeg', 'speex'] 553 @type provides: list of str 554 @param accepts: formats accepted, eg ['theora'] 555 @type accepts: list of str 556 557 @returns: a deferred firing a list 558 of L{flumotion.common.componentui.WizardEntryState} 559 @rtype: L{twisted.internet.defer.Deferred} 560 """ 561 self.debug('querying wizard entries (wizardTypes=%r,provides=%r' 562 ',accepts=%r)'% (wizardTypes, provides, accepts)) 563 return self._adminModel.getWizardEntries(wizardTypes=wizardTypes, 564 provides=provides, 565 accepts=accepts)
566
567 - def setExistingComponentNames(self, componentNames):
568 """Tells the wizard about the existing components available, so 569 we can resolve naming conflicts when saving the configuration 570 @param componentNames: existing component names 571 @type componentNames: list of strings 572 """ 573 self._existingComponentNames = componentNames 574 if self._scenario is not None: 575 self._scenario.setExistingComponentNames(componentNames)
576
577 - def workerChangedForStep(self, step, workerName):
578 """Tell a step that its worker changed. 579 @param step: step which worker changed for 580 @type step: a L{WorkerWizardStep} subclass 581 @param workerName: name of the worker 582 @type workerName: string 583 """ 584 if self._stepWorkers.get(step) == workerName: 585 return 586 587 self.debug('calling %r.workerChanged' % step) 588 step.workerChanged(workerName) 589 self._stepWorkers[step] = workerName
590 591 # Private 592
593 - def _gotEntryPoint(self, (filename, procname)):
594 # The manager always returns / as a path separator, replace them with 595 # the separator since the rest of our infrastructure depends on that. 596 filename = filename.replace('/', os.path.sep) 597 modname = pathToModuleName(filename) 598 d = self._adminModel.getBundledFunction(modname, procname) 599 self.clear_msg('wizard-bundle') 600 self.taskFinished() 601 return d
602
603 - def _setupWorker(self, step, worker):
604 # get name of active worker 605 self.debug('%r setting worker to %s' % (step, worker)) 606 step.worker = worker
607 608 # Callbacks 609
610 - def on_combobox_worker_changed(self, combobox, worker):
611 self.debug('combobox_workerChanged, worker %r' % worker) 612 if worker: 613 self.clear_msg('worker-error') 614 self._lastWorker = worker 615 step = self.getCurrentStep() 616 if step and isinstance(step, WorkerWizardStep): 617 self._setupWorker(step, worker) 618 self.workerChangedForStep(step, worker) 619 else: 620 msg = messages.Error(T_( 621 N_('All workers have logged out.\n' 622 'Make sure your Flumotion network is running ' 623 'properly and try again.')), 624 mid='worker-error') 625 self.add_msg(msg)
626