1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """Configuration Assistant - A graphical user interface to create a stream.
19
20
21 This simple drawing explains the basic user interface:
22
23 +----------+---------------------------------+
24 | | Title |
25 | Sidebar |---------------------------------+
26 | | |
27 | | |
28 | | |
29 | | WizardStep |
30 | | |
31 | | |
32 | | |
33 | | |
34 | | |
35 | +---------------------------------+
36 | | Buttons |
37 +----------+---------------------------------+
38
39 Sidebar shows the available and visited steps, it allows you to quickly
40 navigate back to a previous step.
41 Title and the sidebar name contains text / icon the wizard step can set.
42 Buttons contain navigation and help.
43
44 Most WizardSteps are loaded over the network from the manager (to the admin
45 client where the code runs).
46 """
47 import gettext
48 import os
49 import webbrowser
50
51 import gtk
52 from gtk import gdk
53 from twisted.internet import defer
54
55 from flumotion.admin.assistant.save import AssistantSaver
56 from flumotion.admin.gtk.workerstep import WorkerWizardStep
57 from flumotion.admin.gtk.workerlist import WorkerList
58 from flumotion.common import errors, messages, python
59 from flumotion.common.common import pathToModuleName
60 from flumotion.common import documentation
61 from flumotion.common.i18n import N_, ngettext, gettexter
62 from flumotion.common.pygobject import gsignal
63 from flumotion.configure import configure
64 from flumotion.ui.wizard import SectionWizard, WizardStep
65
66
67
68
69 __pychecker__ = 'no-classattr no-argsused'
70 __version__ = "$Rev$"
71 T_ = gettexter()
72 _ = gettext.gettext
73
74
75
76
77
78
80 """
81 Return a string to be used in serializing to XML.
82 """
83 return "%d/%d" % (number * denominator, denominator)
84
85
87 """
88 This step is showing an informative description which introduces
89 the user to the configuration assistant.
90 """
91 name = "Welcome"
92 title = _('Welcome')
93 section = _('Welcome')
94 icon = 'wizard.png'
95 gladeFile = 'welcome-wizard.glade'
96 docSection = 'help-configuration-assistant-welcome'
97 docAnchor = ''
98 docVersion = 'local'
99
102
103
105 """
106 This step is showing a list of possible scenarios.
107 The user will select the scenario he want to use,
108 then the scenario itself will decide the future steps.
109 """
110 name = "Scenario"
111 title = _('Scenario')
112 section = _('Scenario')
113 icon = 'wizard.png'
114 gladeFile = 'scenario-wizard.glade'
115 docSection = 'help-configuration-assistant-scenario'
116 docAnchor = ''
117 docVersion = 'local'
118
119
120
122 self._currentScenarioType = None
123 self._radioGroup = None
124 self._scenarioRadioButtons = []
125 super(ScenarioStep, self).__init__(wizard)
126
128
129 def addScenarios(list):
130 for scenario in list:
131 self.addScenario(_(scenario.getDescription()),
132 scenario.getType())
133
134 firstButton = self.scenarios_box.get_children()[0]
135 firstButton.set_active(True)
136 firstButton.toggled()
137 firstButton.grab_focus()
138
139 d = self.wizard.getAdminModel().getScenarios()
140 d.addCallback(addScenarios)
141
142 return d
143
145 self.wizard.waitForTask('get-next-step')
146 self.wizard.cleanFutureSteps()
147
148 def addScenarioSteps(scenarioClass):
149 scenario = scenarioClass()
150 scenario.addSteps(self.wizard)
151 self.wizard.setScenario(scenario)
152 self.wizard.taskFinished()
153
154 d = self.wizard.getWizardScenario(self._currentScenarioType)
155 d.addCallback(addScenarioSteps)
156
157 return d
158
159
160
162 """
163 Adds a new entry to the scenarios list of the wizard.
164
165 @param scenarioDesc: Description that will be shown on the list.
166 @type scenarioDesc: str
167 @param scenarioType: The type of the scenario we are adding.
168 @type scenarioType: str
169 """
170 button = gtk.RadioButton(self._radioGroup, scenarioDesc)
171 button.connect('toggled',
172 self._on_radiobutton__toggled,
173 scenarioType)
174 button.connect('activate',
175 self._on_radiobutton__activate)
176
177 self.scenarios_box.pack_start(button, False, False)
178 button.show()
179
180 if self._radioGroup is None:
181 self._radioGroup = button
182
183
184
185
186
189
193
194
196 """This is the main configuration assistant class,
197 it is responsible for::
198 - executing tasks which will block the ui
199 - showing a worker list in the UI
200 - communicating with the manager, fetching bundles
201 and registry information
202 - running check defined by a step in a worker, for instance
203 querying for hardware devices and their capabilities
204 It extends SectionWizard which provides the basic user interface, such
205 as sidebar, buttons, title bar and basic step navigation.
206 """
207 gsignal('finished', str)
208
210 SectionWizard.__init__(self, parent)
211 self.connect('help-clicked', self._on_assistant__help_clicked)
212
213
214 self.window1.set_name('ConfigurationAssistant')
215 self.message_area.disableTimestamps()
216
217 self._cursorWatch = gdk.Cursor(gdk.WATCH)
218 self._tasks = []
219 self._adminModel = None
220 self._workerHeavenState = None
221 self._lastWorker = 0
222 self._stepWorkers = {}
223 self._scenario = None
224 self._existingComponentNames = []
225 self._porters = []
226 self._mountPoints = []
227 self._consumers = {}
228
229 self._workerList = WorkerList()
230 self.top_vbox.pack_start(self._workerList, False, False)
231 self._workerList.connect('worker-selected',
232 self.on_combobox_worker_changed)
233
234
235
244
246 SectionWizard.destroy(self)
247 self._adminModel = None
248
260
264
266
267 if self._tasks:
268 return
269 SectionWizard.blockNext(self, block)
270
271
272
273
274
276 """Add the step sections of the wizard, can be
277 overridden in a subclass
278 """
279
280
281 self.addStepSection(WelcomeStep)
282 self.addStepSection(ScenarioStep)
283
285 """Sets the current scenario of the assistant.
286 Normally called by ScenarioStep to tell the assistant the
287 current scenario just after creating it.
288 @param scenario: the scenario of the assistant
289 @type scenario: a L{flumotion.admin.assistant.scenarios.Scenario}
290 subclass
291 """
292 self._scenario = scenario
293
295 """Fetches the currently set scenario of the assistant.
296 @returns scenario: the scenario of the assistant
297 @rtype: a L{flumotion.admin.assistant.scenarios.Scenario} subclass
298 """
299 return self._scenario
300
302 """
303 Sets the worker heaven state of the assistant
304 @param workerHeavenState: the worker heaven state
305 @type workerHeavenState: L{WorkerComponentUIState}
306 """
307 self._workerHeavenState = workerHeavenState
308 self._workerList.setWorkerHeavenState(workerHeavenState)
309
311 """
312 Sets the admin model of the assistant
313 @param adminModel: the admin model
314 @type adminModel: L{AdminModel}
315 """
316 self._adminModel = adminModel
317 self._adminModel.connect('connected',
318 self.on_admin_connected_cb)
319 self._adminModel.connect('disconnected',
320 self.on_admin_disconnected_cb)
321
323 """
324 Sets the list of currently configured porters so
325 we can reuse them for future streamers.
326
327 @param porters: list of porters
328 @type porters : list of L{flumotion.admin.assistant.models.Porter}
329 """
330
331 self._porters = porters
332
334 """
335 Obtains the list of the currently configured porters.
336
337 @rtype : list of L{flumotion.admin.assistant.models.Porter}
338 """
339 return self._porters
340
341 - def addMountPoint(self, worker, port, mount_point, consumer=None):
342 """
343 Marks a mount point as used on the given worker and port.
344 If a consumer name is provided it means we are changing the
345 mount point for that consumer and that we should keep track of
346 it for further modifications.
347
348 @param worker : The worker where the mount_point is configured.
349 @type worker : str
350 @param port : The port where the streamer should be listening.
351 @type port : int
352 @param mount_point : The mount point where the data will be served.
353 @type mount_point : str
354 @param consumer : The consumer that is changing its mountpoint.
355 @type consumer : str
356
357 @returns : True if the mount point is not used and has been
358 inserted correctly, False otherwise.
359 @rtype : boolean
360 """
361 if not worker or not port or not mount_point:
362 return False
363
364 if consumer in self._consumers:
365 oldData = self._consumers[consumer]
366 if oldData in self._mountPoints:
367 self._mountPoints.remove(oldData)
368
369 data = (worker, port, mount_point)
370
371 if data in self._mountPoints:
372 return False
373
374 self._mountPoints.append(data)
375
376 if consumer:
377 self._consumers[consumer] = data
378
379 return True
380
382 """Gets the admin model of the assistant
383 @returns adminModel: the admin model
384 @rtype adminModel: L{AdminModel}
385 """
386 return self._adminModel
387
389 """Instruct the assistant that we're waiting for a task
390 to be finished. This changes the cursor and prevents
391 the user from continuing moving forward.
392 Each call to this method should have another call
393 to taskFinished() when the task is actually done.
394 @param taskName: name of the name
395 @type taskName: string
396 """
397 self.info("waiting for task %s" % (taskName, ))
398 if not self._tasks:
399 if self.window1.window is not None:
400 self.window1.window.set_cursor(self._cursorWatch)
401 self.blockNext(True)
402 self._tasks.append(taskName)
403
405 """Instruct the assistant that a task was finished.
406 @param blockNext: if we should still next when done
407 @type blockNext: boolean
408 """
409 if not self._tasks:
410 raise AssertionError(
411 "Stray call to taskFinished(), forgot to call waitForTask()?")
412
413 taskName = self._tasks.pop()
414 self.info("task %s has now finished" % (taskName, ))
415 if not self._tasks:
416 self.window1.window.set_cursor(None)
417 self.blockNext(blockNext)
418
420 """Returns true if there are any pending tasks
421 @returns: if there are pending tasks
422 @rtype: bool
423 """
424 return bool(self._tasks)
425
427 """Check if the given list of GStreamer elements exist on the
428 given worker.
429 @param workerName: name of the worker to check on
430 @type workerName: string
431 @param elementNames: names of the elements to check
432 @type elementNames: list of strings
433 @returns: a deferred returning a tuple of the missing elements
434 @rtype: L{twisted.internet.defer.Deferred}
435 """
436 if not self._adminModel:
437 self.debug('No admin connected, not checking presence of elements')
438 return
439
440 asked = python.set(elementNames)
441
442 def _checkElementsCallback(existing, workerName):
443 existing = python.set(existing)
444 self.taskFinished()
445 return tuple(asked.difference(existing))
446
447 self.waitForTask('check elements %r' % (elementNames, ))
448 d = self._adminModel.checkElements(workerName, elementNames)
449 d.addCallback(_checkElementsCallback, workerName)
450 return d
451
453 """Require that the given list of GStreamer elements exists on the
454 given worker. If the elements do not exist, an error message is
455 posted and the next button remains blocked.
456 @param workerName: name of the worker to check on
457 @type workerName: string
458 @param elementNames: names of the elements to check
459 @type elementNames: list of strings
460 @returns: element name
461 @rtype: deferred -> list of strings
462 """
463 if not self._adminModel:
464 self.debug('No admin connected, not checking presence of elements')
465 return
466
467 self.debug('requiring elements %r' % (elementNames, ))
468 f = ngettext("Checking the existence of GStreamer element '%s' "
469 "on %s worker.",
470 "Checking the existence of GStreamer elements '%s' "
471 "on %s worker.",
472 len(elementNames))
473 msg = messages.Info(T_(f, "', '".join(elementNames), workerName),
474 mid='require-elements')
475
476 self.add_msg(msg)
477
478 def gotMissingElements(elements, workerName):
479 self.clear_msg('require-elements')
480
481 if elements:
482 self.warning('elements %r do not exist' % (elements, ))
483 f = ngettext("Worker '%s' is missing GStreamer element '%s'.",
484 "Worker '%s' is missing GStreamer elements '%s'.",
485 len(elements))
486 message = messages.Error(T_(f, workerName,
487 "', '".join(elements)))
488 message.add(T_(N_("\n"
489 "Please install the necessary GStreamer plug-ins that "
490 "provide these elements and restart the worker.")))
491 message.add(T_(N_("\n\n"
492 "You will not be able to go forward using this worker.")))
493 message.id = 'element' + '-'.join(elementNames)
494 documentation.messageAddGStreamerInstall(message)
495 self.add_msg(message)
496 self.taskFinished(bool(elements))
497 return elements
498
499 self.waitForTask('require elements %r' % (elementNames, ))
500 d = self.checkElements(workerName, *elementNames)
501 d.addCallback(gotMissingElements, workerName)
502
503 return d
504
506 """Check if the given module can be imported.
507 @param workerName: name of the worker to check on
508 @type workerName: string
509 @param moduleName: name of the module to import
510 @type moduleName: string
511 @returns: a deferred firing None or Failure.
512 @rtype: L{twisted.internet.defer.Deferred}
513 """
514 if not self._adminModel:
515 self.debug('No admin connected, not checking presence of elements')
516 return
517
518 d = self._adminModel.checkImport(workerName, moduleName)
519 return d
520
521 - def requireImport(self, workerName, moduleName, projectName=None,
522 projectURL=None):
523 """Require that the given module can be imported on the given worker.
524 If the module cannot be imported, an error message is
525 posted and the next button remains blocked.
526 @param workerName: name of the worker to check on
527 @type workerName: string
528 @param moduleName: name of the module to import
529 @type moduleName: string
530 @param projectName: name of the module to import
531 @type projectName: string
532 @param projectURL: URL of the project
533 @type projectURL: string
534 @returns: a deferred firing None or Failure
535 @rtype: L{twisted.internet.defer.Deferred}
536 """
537 if not self._adminModel:
538 self.debug('No admin connected, not checking presence of elements')
539 return
540
541 self.debug('requiring module %s' % moduleName)
542
543 def _checkImportErrback(failure):
544 self.warning('could not import %s', moduleName)
545 message = messages.Error(T_(N_(
546 "Worker '%s' cannot import module '%s'."),
547 workerName, moduleName))
548 if projectName:
549 message.add(T_(N_("\n"
550 "This module is part of '%s'."), projectName))
551 if projectURL:
552 message.add(T_(N_("\n"
553 "The project's homepage is %s"), projectURL))
554 message.add(T_(N_("\n\n"
555 "You will not be able to go forward using this worker.")))
556 message.id = 'module-%s' % moduleName
557 documentation.messageAddPythonInstall(message, moduleName)
558 self.add_msg(message)
559 self.taskFinished(blockNext=True)
560 return False
561
562 d = self.checkImport(workerName, moduleName)
563 d.addErrback(_checkImportErrback)
564 return d
565
566
567
568 - def runInWorker(self, workerName, moduleName, functionName,
569 *args, **kwargs):
570 """
571 Run the given function and arguments on the selected worker.
572 The given function should return a L{messages.Result}.
573
574 @param workerName: name of the worker to run the function in
575 @type workerName: string
576 @param moduleName: name of the module where the function is found
577 @type moduleName: string
578 @param functionName: name of the function to run
579 @type functionName: string
580
581 @returns: a deferred firing the Result's value.
582 @rtype: L{twisted.internet.defer.Deferred}
583 """
584 self.debug('runInWorker(moduleName=%r, functionName=%r)' % (
585 moduleName, functionName))
586 admin = self._adminModel
587 if not admin:
588 self.warning('skipping runInWorker, no admin')
589 return defer.fail(errors.FlumotionError('no admin'))
590
591 if not workerName:
592 self.warning('skipping runInWorker, no worker')
593 return defer.fail(errors.FlumotionError('no worker'))
594
595 def callback(result):
596 self.debug('runInWorker callbacked a result')
597 self.clear_msg(functionName)
598
599 if not isinstance(result, messages.Result):
600 msg = messages.Error(T_(
601 N_("Internal error: could not run check code on worker.")),
602 debug=('function %r returned a non-Result %r'
603 % (functionName, result)))
604 self.add_msg(msg)
605 self.taskFinished(True)
606 raise errors.RemoteRunError(functionName, 'Internal error.')
607
608 for m in result.messages:
609 self.debug('showing msg %r' % m)
610 self.add_msg(m)
611
612 if result.failed:
613 self.debug('... that failed')
614 self.taskFinished(True)
615 raise errors.RemoteRunFailure(functionName, 'Result failed')
616 self.debug('... that succeeded')
617 self.taskFinished()
618 return result.value
619
620 def errback(failure):
621 self.debug('runInWorker errbacked, showing error msg')
622 if failure.check(errors.RemoteRunError):
623 debug = failure.value
624 else:
625 debug = "Failure while running %s.%s:\n%s" % (
626 moduleName, functionName, failure.getTraceback())
627
628 msg = messages.Error(T_(
629 N_("Internal error: could not run check code on worker.")),
630 debug=debug)
631 self.add_msg(msg)
632 self.taskFinished(True)
633 raise errors.RemoteRunError(functionName, 'Internal error.')
634
635 self.waitForTask('run in worker: %s.%s(%r, %r)' % (
636 moduleName, functionName, args, kwargs))
637 d = admin.workerRun(workerName, moduleName,
638 functionName, *args, **kwargs)
639 d.addErrback(errback)
640 d.addCallback(callback)
641 return d
642
643 - def getWizardEntry(self, componentType):
644 """Fetches a assistant bundle from a specific kind of component
645 @param componentType: the component type to get the assistant entry
646 bundle from.
647 @type componentType: string
648 @returns: a deferred returning either::
649 - factory of the component
650 - noBundle error: if the component lacks a assistant bundle
651 @rtype: L{twisted.internet.defer.Deferred}
652 """
653 self.waitForTask('get assistant entry %s' % (componentType, ))
654 self.clear_msg('assistant-bundle')
655 d = self._adminModel.callRemote(
656 'getEntryByType', componentType, 'wizard')
657 d.addCallback(self._gotEntryPoint)
658 return d
659
661 """
662 Fetches a scenario bundle from a specific kind of component.
663
664 @param scenarioType: the scenario type to get the assistant entry
665 bundle from.
666 @type scenarioType: string
667 @returns: a deferred returning either::
668 - factory of the component
669 - noBundle error: if the component lacks a assistant bundle
670 @rtype: L{twisted.internet.defer.Deferred}
671 """
672 self.waitForTask('get assistant entry %s' % (scenarioType, ))
673 self.clear_msg('assistant-bundle')
674 d = self._adminModel.callRemote(
675 'getScenarioByType', scenarioType, 'wizard')
676 d.addCallback(self._gotEntryPoint)
677 return d
678
679 - def getWizardPlugEntry(self, plugType):
680 """Fetches a assistant bundle from a specific kind of plug
681 @param plugType: the plug type to get the assistant entry
682 bundle from.
683 @type plugType: string
684 @returns: a deferred returning either::
685 - factory of the plug
686 - noBundle error: if the plug lacks a assistant bundle
687 @rtype: L{twisted.internet.defer.Deferred}
688 """
689 self.waitForTask('get assistant plug %s' % (plugType, ))
690 self.clear_msg('assistant-bundle')
691 d = self._adminModel.callRemote(
692 'getPlugEntry', plugType, 'wizard')
693 d.addCallback(self._gotEntryPoint)
694 return d
695
697 """Queries the manager for a list of assistant entries matching the
698 query.
699 @param wizardTypes: list of component types to fetch, is usually
700 something like ['video-producer'] or
701 ['audio-encoder']
702 @type wizardTypes: list of str
703 @param provides: formats provided, eg ['jpeg', 'speex']
704 @type provides: list of str
705 @param accepts: formats accepted, eg ['theora']
706 @type accepts: list of str
707
708 @returns: a deferred firing a list
709 of L{flumotion.common.componentui.WizardEntryState}
710 @rtype: L{twisted.internet.defer.Deferred}
711 """
712 self.debug('querying wizard entries (wizardTypes=%r,provides=%r'
713 ',accepts=%r)'% (wizardTypes, provides, accepts))
714 return self._adminModel.getWizardEntries(wizardTypes=wizardTypes,
715 provides=provides,
716 accepts=accepts)
717
719 """Tells the assistant about the existing components available, so
720 we can resolve naming conflicts when saving the configuration
721 @param componentNames: existing component names
722 @type componentNames: list of strings
723 """
724 self._existingComponentNames = componentNames
725
727 """Tell a step that its worker changed.
728 @param step: step which worker changed for
729 @type step: a L{WorkerWizardStep} subclass
730 @param workerName: name of the worker
731 @type workerName: string
732 """
733 if self._stepWorkers.get(step) == workerName:
734 return
735
736 self.debug('calling %r.workerChanged' % step)
737 step.workerChanged(workerName)
738 self._stepWorkers[step] = workerName
739
740
741
742 - def _gotEntryPoint(self, (filename, procname)):
743
744
745 filename = filename.replace('/', os.path.sep)
746 modname = pathToModuleName(filename)
747 d = self._adminModel.getBundledFunction(modname, procname)
748 self.clear_msg('assistant-bundle')
749 self.taskFinished()
750 return d
751
756
765
767 if not hasattr(step, 'model'):
768 self.setStepDescription('')
769 return
770
771 def gotComponentEntry(entry):
772 self.setStepDescription(entry.description)
773
774 d = self._adminModel.callRemote(
775 'getComponentEntry', step.model.componentType)
776 d.addCallback(gotComponentEntry)
777
778
779
781 self.debug('combobox_workerChanged, worker %r' % worker)
782 if worker:
783 self.clear_msg('worker-error')
784 self._lastWorker = worker
785 step = self.getCurrentStep()
786 if step and isinstance(step, WorkerWizardStep):
787 self._setupWorker(step, worker)
788 self.workerChangedForStep(step, worker)
789 else:
790 msg = messages.Error(T_(
791 N_('All workers have logged out.\n'
792 'Make sure your Flumotion network is running '
793 'properly and try again.')),
794 mid='worker-error')
795 self.add_msg(msg)
796
799
801 self.window1.set_sensitive(True)
802
804 self.window1.set_sensitive(False)
805