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: 8321 $"
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
264
268
270
271 if self._tasks:
272 return
273 SectionWizard.blockNext(self, block)
274
275
276
277
278
280 """Add the step sections of the wizard, can be
281 overridden in a subclass
282 """
283
284
285 self.addStepSection(WelcomeStep)
286 self.addStepSection(ScenarioStep)
287
289 """Sets the current scenario of the assistant.
290 Normally called by ScenarioStep to tell the assistant the
291 current scenario just after creating it.
292 @param scenario: the scenario of the assistant
293 @type scenario: a L{flumotion.admin.assistant.scenarios.Scenario}
294 subclass
295 """
296 self._scenario = scenario
297
299 """Fetches the currently set scenario of the assistant.
300 @returns scenario: the scenario of the assistant
301 @rtype: a L{flumotion.admin.assistant.scenarios.Scenario} subclass
302 """
303 return self._scenario
304
306 """
307 Sets the worker heaven state of the assistant
308 @param workerHeavenState: the worker heaven state
309 @type workerHeavenState: L{WorkerComponentUIState}
310 """
311 self._workerHeavenState = workerHeavenState
312 self._workerList.setWorkerHeavenState(workerHeavenState)
313
315 """
316 Sets the admin model of the assistant
317 @param adminModel: the admin model
318 @type adminModel: L{AdminModel}
319 """
320 self._adminModel = adminModel
321 self._adminModel.connect('connected',
322 self.on_admin_connected_cb)
323 self._adminModel.connect('disconnected',
324 self.on_admin_disconnected_cb)
325
327 """
328 Sets the list of currently configured porters so
329 we can reuse them for future streamers.
330
331 @param porters: list of porters
332 @type porters : list of L{flumotion.admin.assistant.models.Porter}
333 """
334
335 self._porters = porters
336
338 """
339 Obtains the list of the currently configured porters.
340
341 @rtype : list of L{flumotion.admin.assistant.models.Porter}
342 """
343 return self._porters
344
345 - def addMountPoint(self, worker, port, mount_point, consumer=None):
346 """
347 Marks a mount point as used on the given worker and port.
348 If a consumer name is provided it means we are changing the
349 mount point for that consumer and that we should keep track of
350 it for further modifications.
351
352 @param worker : The worker where the mount_point is configured.
353 @type worker : str
354 @param port : The port where the streamer should be listening.
355 @type port : int
356 @param mount_point : The mount point where the data will be served.
357 @type mount_point : str
358 @param consumer : The consumer that is changing its mountpoint.
359 @type consumer : str
360
361 @returns : True if the mount point is not used and has been
362 inserted correctly, False otherwise.
363 @rtype : boolean
364 """
365 if not worker or not port or not mount_point:
366 return False
367
368 if consumer in self._consumers:
369 oldData = self._consumers[consumer]
370 if oldData in self._mountPoints:
371 self._mountPoints.remove(oldData)
372
373 data = (worker, port, mount_point)
374
375 if data in self._mountPoints:
376 return False
377
378 self._mountPoints.append(data)
379
380 if consumer:
381 self._consumers[consumer] = data
382
383 return True
384
386 """Gets the admin model of the assistant
387 @returns adminModel: the admin model
388 @rtype adminModel: L{AdminModel}
389 """
390 return self._adminModel
391
393 """Instruct the assistant that we're waiting for a task
394 to be finished. This changes the cursor and prevents
395 the user from continuing moving forward.
396 Each call to this method should have another call
397 to taskFinished() when the task is actually done.
398 @param taskName: name of the name
399 @type taskName: string
400 """
401 self.info("waiting for task %s" % (taskName, ))
402 if not self._tasks:
403 if self.window1.window is not None:
404 self.window1.window.set_cursor(self._cursorWatch)
405 self.blockNext(True)
406 self._tasks.append(taskName)
407
409 """Instruct the assistant that a task was finished.
410 @param blockNext: if we should still next when done
411 @type blockNext: boolean
412 """
413 if not self._tasks:
414 raise AssertionError(
415 "Stray call to taskFinished(), forgot to call waitForTask()?")
416
417 taskName = self._tasks.pop()
418 self.info("task %s has now finished" % (taskName, ))
419 if not self._tasks:
420 self.window1.window.set_cursor(None)
421 self.blockNext(blockNext)
422
424 """Returns true if there are any pending tasks
425 @returns: if there are pending tasks
426 @rtype: bool
427 """
428 return bool(self._tasks)
429
431 """Check if the given list of GStreamer elements exist on the
432 given worker.
433 @param workerName: name of the worker to check on
434 @type workerName: string
435 @param elementNames: names of the elements to check
436 @type elementNames: list of strings
437 @returns: a deferred returning a tuple of the missing elements
438 @rtype: L{twisted.internet.defer.Deferred}
439 """
440 if not self._adminModel:
441 self.debug('No admin connected, not checking presence of elements')
442 return
443
444 asked = python.set(elementNames)
445
446 def _checkElementsCallback(existing, workerName):
447 existing = python.set(existing)
448 self.taskFinished()
449 return tuple(asked.difference(existing))
450
451 self.waitForTask('check elements %r' % (elementNames, ))
452 d = self._adminModel.checkElements(workerName, elementNames)
453 d.addCallback(_checkElementsCallback, workerName)
454 return d
455
457 """Require that the given list of GStreamer elements exists on the
458 given worker. If the elements do not exist, an error message is
459 posted and the next button remains blocked.
460 @param workerName: name of the worker to check on
461 @type workerName: string
462 @param elementNames: names of the elements to check
463 @type elementNames: list of strings
464 @returns: element name
465 @rtype: deferred -> list of strings
466 """
467 if not self._adminModel:
468 self.debug('No admin connected, not checking presence of elements')
469 return
470
471 self.debug('requiring elements %r' % (elementNames, ))
472 f = ngettext("Checking the existence of GStreamer element '%s' "
473 "on %s worker.",
474 "Checking the existence of GStreamer elements '%s' "
475 "on %s worker.",
476 len(elementNames))
477 msg = messages.Info(T_(f, "', '".join(elementNames), workerName),
478 mid='require-elements')
479
480 self.add_msg(msg)
481
482 def gotMissingElements(elements, workerName):
483 self.clear_msg('require-elements')
484
485 if elements:
486 self.warning('elements %r do not exist' % (elements, ))
487 f = ngettext("Worker '%s' is missing GStreamer element '%s'.",
488 "Worker '%s' is missing GStreamer elements '%s'.",
489 len(elements))
490 message = messages.Error(T_(f, workerName,
491 "', '".join(elements)))
492 message.add(T_(N_("\n"
493 "Please install the necessary GStreamer plug-ins that "
494 "provide these elements and restart the worker.")))
495 message.add(T_(N_("\n\n"
496 "You will not be able to go forward using this worker.")))
497 message.id = 'element' + '-'.join(elementNames)
498 documentation.messageAddGStreamerInstall(message)
499 self.add_msg(message)
500 self.taskFinished(bool(elements))
501 return elements
502
503 self.waitForTask('require elements %r' % (elementNames, ))
504 d = self.checkElements(workerName, *elementNames)
505 d.addCallback(gotMissingElements, workerName)
506
507 return d
508
510 """Check if the given module can be imported.
511 @param workerName: name of the worker to check on
512 @type workerName: string
513 @param moduleName: name of the module to import
514 @type moduleName: string
515 @returns: a deferred firing None or Failure.
516 @rtype: L{twisted.internet.defer.Deferred}
517 """
518 if not self._adminModel:
519 self.debug('No admin connected, not checking presence of elements')
520 return
521
522 d = self._adminModel.checkImport(workerName, moduleName)
523 return d
524
525 - def requireImport(self, workerName, moduleName, projectName=None,
526 projectURL=None):
527 """Require that the given module can be imported on the given worker.
528 If the module cannot be imported, an error message is
529 posted and the next button remains blocked.
530 @param workerName: name of the worker to check on
531 @type workerName: string
532 @param moduleName: name of the module to import
533 @type moduleName: string
534 @param projectName: name of the module to import
535 @type projectName: string
536 @param projectURL: URL of the project
537 @type projectURL: string
538 @returns: a deferred firing None or Failure
539 @rtype: L{twisted.internet.defer.Deferred}
540 """
541 if not self._adminModel:
542 self.debug('No admin connected, not checking presence of elements')
543 return
544
545 self.debug('requiring module %s' % moduleName)
546
547 def _checkImportErrback(failure):
548 self.warning('could not import %s', moduleName)
549 message = messages.Error(T_(N_(
550 "Worker '%s' cannot import module '%s'."),
551 workerName, moduleName))
552 if projectName:
553 message.add(T_(N_("\n"
554 "This module is part of '%s'."), projectName))
555 if projectURL:
556 message.add(T_(N_("\n"
557 "The project's homepage is %s"), projectURL))
558 message.add(T_(N_("\n\n"
559 "You will not be able to go forward using this worker.")))
560 message.id = 'module-%s' % moduleName
561 documentation.messageAddPythonInstall(message, moduleName)
562 self.add_msg(message)
563 self.taskFinished(blockNext=True)
564 return False
565
566 d = self.checkImport(workerName, moduleName)
567 d.addErrback(_checkImportErrback)
568 return d
569
570
571
572 - def runInWorker(self, workerName, moduleName, functionName,
573 *args, **kwargs):
574 """
575 Run the given function and arguments on the selected worker.
576 The given function should return a L{messages.Result}.
577
578 @param workerName: name of the worker to run the function in
579 @type workerName: string
580 @param moduleName: name of the module where the function is found
581 @type moduleName: string
582 @param functionName: name of the function to run
583 @type functionName: string
584
585 @returns: a deferred firing the Result's value.
586 @rtype: L{twisted.internet.defer.Deferred}
587 """
588 self.debug('runInWorker(moduleName=%r, functionName=%r)' % (
589 moduleName, functionName))
590 admin = self._adminModel
591 if not admin:
592 self.warning('skipping runInWorker, no admin')
593 return defer.fail(errors.FlumotionError('no admin'))
594
595 if not workerName:
596 self.warning('skipping runInWorker, no worker')
597 return defer.fail(errors.FlumotionError('no worker'))
598
599 def callback(result):
600 self.debug('runInWorker callbacked a result')
601 self.clear_msg(functionName)
602
603 if not isinstance(result, messages.Result):
604 msg = messages.Error(T_(
605 N_("Internal error: could not run check code on worker.")),
606 debug=('function %r returned a non-Result %r'
607 % (functionName, result)))
608 self.add_msg(msg)
609 self.taskFinished(True)
610 raise errors.RemoteRunError(functionName, 'Internal error.')
611
612 for m in result.messages:
613 self.debug('showing msg %r' % m)
614 self.add_msg(m)
615
616 if result.failed:
617 self.debug('... that failed')
618 self.taskFinished(True)
619 raise errors.RemoteRunFailure(functionName, 'Result failed')
620 self.debug('... that succeeded')
621 self.taskFinished()
622 return result.value
623
624 def errback(failure):
625 self.debug('runInWorker errbacked, showing error msg')
626 if failure.check(errors.RemoteRunError):
627 debug = failure.value
628 else:
629 debug = "Failure while running %s.%s:\n%s" % (
630 moduleName, functionName, failure.getTraceback())
631
632 msg = messages.Error(T_(
633 N_("Internal error: could not run check code on worker.")),
634 debug=debug)
635 self.add_msg(msg)
636 self.taskFinished(True)
637 raise errors.RemoteRunError(functionName, 'Internal error.')
638
639 self.waitForTask('run in worker: %s.%s(%r, %r)' % (
640 moduleName, functionName, args, kwargs))
641 d = admin.workerRun(workerName, moduleName,
642 functionName, *args, **kwargs)
643 d.addErrback(errback)
644 d.addCallback(callback)
645 return d
646
647 - def getWizardEntry(self, componentType):
648 """Fetches a assistant bundle from a specific kind of component
649 @param componentType: the component type to get the assistant entry
650 bundle from.
651 @type componentType: string
652 @returns: a deferred returning either::
653 - factory of the component
654 - noBundle error: if the component lacks a assistant bundle
655 @rtype: L{twisted.internet.defer.Deferred}
656 """
657 self.waitForTask('get assistant entry %s' % (componentType, ))
658 self.clear_msg('assistant-bundle')
659 d = self._adminModel.callRemote(
660 'getEntryByType', componentType, 'wizard')
661 d.addCallback(self._gotEntryPoint)
662 return d
663
665 """
666 Fetches a scenario bundle from a specific kind of component.
667
668 @param scenarioType: the scenario type to get the assistant entry
669 bundle from.
670 @type scenarioType: string
671 @returns: a deferred returning either::
672 - factory of the component
673 - noBundle error: if the component lacks a assistant bundle
674 @rtype: L{twisted.internet.defer.Deferred}
675 """
676 self.waitForTask('get assistant entry %s' % (scenarioType, ))
677 self.clear_msg('assistant-bundle')
678 d = self._adminModel.callRemote(
679 'getScenarioByType', scenarioType, 'wizard')
680 d.addCallback(self._gotEntryPoint)
681 return d
682
683 - def getWizardPlugEntry(self, plugType):
684 """Fetches a assistant bundle from a specific kind of plug
685 @param plugType: the plug type to get the assistant entry
686 bundle from.
687 @type plugType: string
688 @returns: a deferred returning either::
689 - factory of the plug
690 - noBundle error: if the plug lacks a assistant bundle
691 @rtype: L{twisted.internet.defer.Deferred}
692 """
693 self.waitForTask('get assistant plug %s' % (plugType, ))
694 self.clear_msg('assistant-bundle')
695 d = self._adminModel.callRemote(
696 'getPlugEntry', plugType, 'wizard')
697 d.addCallback(self._gotEntryPoint)
698 return d
699
701 """Queries the manager for a list of assistant entries matching the
702 query.
703 @param wizardTypes: list of component types to fetch, is usually
704 something like ['video-producer'] or
705 ['audio-encoder']
706 @type wizardTypes: list of str
707 @param provides: formats provided, eg ['jpeg', 'speex']
708 @type provides: list of str
709 @param accepts: formats accepted, eg ['theora']
710 @type accepts: list of str
711
712 @returns: a deferred firing a list
713 of L{flumotion.common.componentui.WizardEntryState}
714 @rtype: L{twisted.internet.defer.Deferred}
715 """
716 self.debug('querying wizard entries (wizardTypes=%r,provides=%r'
717 ',accepts=%r)'% (wizardTypes, provides, accepts))
718 return self._adminModel.getWizardEntries(wizardTypes=wizardTypes,
719 provides=provides,
720 accepts=accepts)
721
723 """Tells the assistant about the existing components available, so
724 we can resolve naming conflicts when saving the configuration
725 @param componentNames: existing component names
726 @type componentNames: list of strings
727 """
728 self._existingComponentNames = componentNames
729
731 """Tell a step that its worker changed.
732 @param step: step which worker changed for
733 @type step: a L{WorkerWizardStep} subclass
734 @param workerName: name of the worker
735 @type workerName: string
736 """
737 if self._stepWorkers.get(step) == workerName:
738 return
739
740 self.debug('calling %r.workerChanged' % step)
741 step.workerChanged(workerName)
742 self._stepWorkers[step] = workerName
743
744
745
746 - def _gotEntryPoint(self, (filename, procname)):
747
748
749 filename = filename.replace('/', os.path.sep)
750 modname = pathToModuleName(filename)
751 d = self._adminModel.getBundledFunction(modname, procname)
752 self.clear_msg('assistant-bundle')
753 self.taskFinished()
754 return d
755
760
769
771 if not hasattr(step, 'model'):
772 self.setStepDescription('')
773 return
774
775 def gotComponentEntry(entry):
776 self.setStepDescription(entry.description)
777
778 d = self._adminModel.callRemote(
779 'getComponentEntry', step.model.componentType)
780 d.addCallback(gotComponentEntry)
781
782
783
785 self.debug('combobox_workerChanged, worker %r' % worker)
786 if worker:
787 self.clear_msg('worker-error')
788 self._lastWorker = worker
789 step = self.getCurrentStep()
790 if step and isinstance(step, WorkerWizardStep):
791 self._setupWorker(step, worker)
792 self.workerChangedForStep(step, worker)
793 else:
794 msg = messages.Error(T_(
795 N_('All workers have logged out.\n'
796 'Make sure your Flumotion network is running '
797 'properly and try again.')),
798 mid='worker-error')
799 self.add_msg(msg)
800
803
805 self.window1.set_sensitive(True)
806
808 self.window1.set_sensitive(False)
809