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