1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import gettext
23 import os
24 import sys
25
26 import gobject
27 import gtk
28 import gtk.glade
29 from twisted.internet import reactor
30 from twisted.internet.defer import maybeDeferred
31 from zope.interface import implements
32
33 from flumotion.admin.admin import AdminModel
34 from flumotion.admin import connections
35 from flumotion.admin.gtk import dialogs, parts
36 from flumotion.admin.gtk.parts import getComponentLabel
37 from flumotion.admin.gtk import connections as gtkconnections
38 from flumotion.configure import configure
39 from flumotion.common import errors, log, planet, pygobject
40 from flumotion.common import connection
41 from flumotion.manager import admin
42 from flumotion.twisted import flavors, pb as fpb
43 from flumotion.ui import trayicon
44 from flumotion.common.planet import moods
45 from flumotion.common.pygobject import gsignal
46
47 from flumotion.common import messages
48
49 _ = gettext.gettext
50 T_ = messages.gettexter('flumotion')
51
52 MAIN_UI = """
53 <ui>
54 <menubar name="menubar">
55 <menu action="connection">
56 <menuitem action="open-recent"/>
57 <menuitem action="open-existing"/>
58 <menuitem action="import-config"/>
59 <menuitem action="export-config"/>
60 <separator name="sep1"/>
61 <placeholder name="recent"/>
62 <separator name="sep2"/>
63 <menuitem action="quit"/>
64 </menu>
65 <menu action="manage">
66 <menuitem action="start-component"/>
67 <menuitem action="stop-component"/>
68 <menuitem action="delete-component"/>
69 <separator name="sep3"/>
70 <menuitem action="start-all"/>
71 <menuitem action="stop-all"/>
72 <menuitem action="clear-all"/>
73 <separator name="sep4"/>
74 <menuitem action="run-wizard"/>
75 </menu>
76 <menu action="debug">
77 <menuitem action="reload-manager"/>
78 <menuitem action="reload-admin"/>
79 <menuitem action="reload-all"/>
80 <menuitem action="start-shell"/>
81 </menu>
82 <menu action="help">
83 <menuitem action="about"/>
84 </menu>
85 </menubar>
86 <toolbar name="toolbar">
87 <toolitem action="open-recent"/>
88 <separator name="sep5"/>
89 <toolitem action="start-component"/>
90 <toolitem action="stop-component"/>
91 <toolitem action="delete-component"/>
92 <separator name="sep6"/>
93 <toolitem action="run-wizard"/>
94 </toolbar>
95 </ui>
96 """
97
98 RECENT_UI_TEMPLATE = '''<ui>
99 <menubar name="menubar">
100 <menu action="connection">
101 <placeholder name="recent">
102 %s
103 </placeholder>
104 </menu>
105 </menubar>
106 </ui>'''
107
108 MAX_RECENT_ITEMS = 4
109
110
111 -class Window(log.Loggable, gobject.GObject):
112 '''
113 Creates the GtkWindow for the user interface.
114 Also connects to the manager on the given host and port.
115 '''
116
117 implements(flavors.IStateListener)
118
119 logCategory = 'adminview'
120 gsignal('connected')
121
123 gobject.GObject.__init__(self)
124
125 self._trayicon = None
126 self._current_component_state = None
127 self._disconnected_dialog = None
128 self._planetState = None
129 self._components = None
130 self._wizard = None
131 self._admin = None
132 self._widgets = {}
133 self._window = None
134 self._recent_menu_uid = None
135
136 self._create_ui()
137 self._append_recent_connections()
138
139
140
154
176 def eb(failure, self, mid):
177 if mid:
178 self.statusbar.remove('main', mid)
179 self.warning("Failed to execute %s on component %s: %s"
180 % (methodName, label, failure))
181 if fail:
182 self.statusbar.push('main', fail % label)
183
184 d.addCallback(cb, self, mid)
185 d.addErrback(eb, self, mid)
186
190
192 if key == 'names':
193 self.statusbar.set('main', 'Worker %s logged in.' % value)
194
196 if key == 'names':
197 self.statusbar.set('main', 'Worker %s logged out.' % value)
198
201
226
227
228
230 self.debug('creating UI')
231
232
233 wtree = gtk.glade.XML(os.path.join(configure.gladedir, 'admin.glade'))
234 wtree.signal_autoconnect(self)
235
236 widgets = self._widgets
237 for widget in wtree.get_widget_prefix(''):
238 widgets[widget.get_name()] = widget
239
240 window = self._window = widgets['main_window']
241 vbox = widgets['vbox1']
242 window.connect('delete-event', self._window_delete_event_cb)
243
244 actions = [
245
246 ('connection', None, _("_Connection")),
247 ('open-recent', gtk.STOCK_OPEN, _('_Open Recent Connection...'), None,
248 None, self._connection_open_recent_cb),
249 ('open-existing', None, _('Open _Existing Connection...'), None,
250 None, self._connection_open_existing_cb),
251 ('import-config', None, _('_Import Configuration...'), None,
252 None, self._connection_import_configuration_cb),
253 ('export-config', None, _('_Export Configuration...'), None,
254 None, self._connection_export_configuration_cb),
255 ('quit', gtk.STOCK_QUIT, _('_Quit'), None,
256 None, self._connection_quit_cb),
257
258
259 ('manage', None, _('_Manage')),
260 ('start-component', 'flumotion-play', _('_S_tart Component'), None,
261 None, self._manage_start_component_cb),
262 ('stop-component', 'flumotion-pause', _('St_op Component'), None,
263 None, self._manage_stop_component_cb),
264 ('delete-component', gtk.STOCK_DELETE, _('_Delete Component'), None,
265 None, self._manage_delete_component_cb),
266 ('start-all', None, _('Start _All'), None,
267 None, self._manage_start_all_cb),
268 ('stop-all', None, _('Stop A_ll'), None,
269 None, self._manage_stop_all_cb),
270 ('clear-all', gtk.STOCK_CLEAR, _('_Clear All'), None,
271 None, self._manage_clear_all_cb),
272 ('run-wizard', 'flumotion-wizard', _('Run _Wizard'), None,
273 None, self._manage_run_wizard_cb),
274
275
276 ('debug', None, _('_Debug')),
277 ('reload-manager', gtk.STOCK_REFRESH, _('Reload _Manager'), None,
278 None, self._debug_reload_manager_cb),
279 ('reload-admin', gtk.STOCK_REFRESH, _('Reload _Admin'), None,
280 None, self._debug_reload_admin_cb),
281 ('reload-all', gtk.STOCK_REFRESH, _('Reload A_ll'), None,
282 None, self._debug_reload_all_cb),
283 ('start-shell', gtk.STOCK_EXECUTE, _('Start _Shell'), None,
284 None, self._debug_start_shell_cb),
285
286
287 ('help', None, _('_Help')),
288 ('about', gtk.STOCK_ABOUT, _('_About'), None,
289 None, self._help_about_cb),
290 ]
291 uimgr = gtk.UIManager()
292 group = gtk.ActionGroup('actions')
293 group.add_actions(actions)
294 uimgr.insert_action_group(group, 0)
295 uimgr.add_ui_from_string(MAIN_UI)
296 window.add_accel_group(uimgr.get_accel_group())
297 menubar = uimgr.get_widget('/menubar')
298 vbox.pack_start(menubar, expand=False)
299 vbox.reorder_child(menubar, 0)
300
301 toolbar = uimgr.get_widget('/toolbar')
302 toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)
303 toolbar.set_style(gtk.TOOLBAR_ICONS)
304 vbox.pack_start(toolbar, expand=False)
305 vbox.reorder_child(toolbar, 1)
306
307 menubar.show_all()
308
309 self._actiongroup = group
310 self._uimgr = uimgr
311 self._start_component_action = group.get_action("start-component")
312 self._stop_component_action = group.get_action("stop-component")
313 self._delete_component_action = group.get_action("delete-component")
314 self._stop_all_action = group.get_action("stop-all")
315 self._start_all_action = group.get_action("start-all")
316 self._clear_all_action = group.get_action("clear-all")
317
318 self._trayicon = trayicon.FluTrayIcon(window)
319 self._trayicon.connect("quit", self._trayicon_quit_cb)
320 self._trayicon.set_tooltip(_('Not connected'))
321
322
323 self._component_view = widgets['component_view']
324
325 self.components_view = parts.ComponentsView(widgets['components_view'])
326 self.components_view.connect('selection_changed',
327 self._components_view_selection_changed_cb)
328 self.components_view.connect('activated',
329 self._components_view_activated_cb)
330 self.statusbar = parts.AdminStatusbar(widgets['statusbar'])
331 self._update_component_actions()
332 self.components_view.connect(
333 'notify::can-start-any',
334 self._component_view_start_stop_notify_cb)
335 self.components_view.connect(
336 'notify::can-stop-any',
337 self._component_view_start_stop_notify_cb)
338 self._component_view_start_stop_notify_cb()
339
340 self._messages_view = widgets['messages_view']
341 self._messages_view.hide()
342
343 return window
344
346 if self._recent_menu_uid:
347 self._uimgr.remove_ui(self._recent_menu_uid)
348 self._uimgr.ensure_update()
349
350 def recent_activate(action, conn):
351 self._open_connection(conn['info'])
352
353 ui = ""
354 for conn in connections.get_recent_connections()[:MAX_RECENT_ITEMS]:
355 name = conn['name']
356 ui += '<menuitem action="%s"/>' % name
357 action = gtk.Action(name, name, '', '')
358 action.connect('activate', recent_activate, conn)
359 self._actiongroup.add_action(action)
360
361 self._recent_menu_uid = self._uimgr.add_ui_from_string(
362 RECENT_UI_TEMPLATE % ui)
363
366
368 import pprint
369 import cStringIO
370 fd = cStringIO.StringIO()
371 pprint.pprint(configation, fd)
372 fd.seek(0)
373 self.debug('Configuration=%s' % fd.read())
374
376 if self._wizard:
377 self._wizard.present()
378
379 def _wizard_finished_cb(wizard, configuration):
380 wizard.destroy()
381 self._dump_config(configuration)
382 self._admin.loadConfiguration(configuration)
383 self.show()
384
385 def nullwizard(*args):
386 self._wizard = None
387
388 state = self._admin.getWorkerHeavenState()
389 if not state.get('names'):
390 self._error(
391 _('The wizard cannot be run because no workers are logged in.'))
392 return
393
394 from flumotion.wizard.configurationwizard import ConfigurationWizard
395 wizard = ConfigurationWizard(self._window, self._admin)
396 wizard.connect('finished', _wizard_finished_cb)
397 wizard.run(True, state, False)
398
399 self._wizard = wizard
400 self._wizard.connect('destroy', nullwizard)
401
416
417 def refused(failure):
418 if failure.check(errors.ConnectionRefusedError):
419 d = dialogs.connection_refused_message(i.host,
420 self._window)
421 else:
422 d = dialogs.connection_failed_message(i, str(failure),
423 self._window)
424 d.addCallback(lambda _: self._window.set_sensitive(True))
425
426 d.addCallbacks(connected, refused)
427 self._window.set_sensitive(False)
428
430 state = self._current_component_state
431 if state:
432 moodname = moods.get(state.get('mood')).name
433 can_start = moodname == 'sleeping'
434 can_stop = moodname != 'sleeping'
435 else:
436 can_start = False
437 can_stop = False
438 can_delete = bool(state and not can_stop)
439 can_start_all = self.components_view.get_property('can-start-any')
440 can_stop_all = self.components_view.get_property('can-stop-any')
441
442 can_clear_all = can_start_all and not can_stop_all
443
444 self._stop_all_action.set_sensitive(can_stop_all)
445 self._start_all_action.set_sensitive(can_start_all)
446 self._clear_all_action.set_sensitive(can_clear_all)
447 self._start_component_action.set_sensitive(can_start)
448 self._stop_component_action.set_sensitive(can_stop)
449 self._delete_component_action.set_sensitive(can_delete)
450 self.debug('can start %r, can stop %r' % (can_start, can_stop))
451
453 self.components_view.update(self._components)
454 self._trayicon.update(self._components)
455
457 self._messages_view.clear()
458 pstate = self._planetState
459 if pstate and pstate.hasKey('messages'):
460 for message in pstate.get('messages').values():
461 self._messages_view.add_message(message)
462
471
472 def flowStateRemove(state, key, value):
473 if key == 'components':
474 self._remove_component(value)
475
476 def atmosphereStateAppend(state, key, value):
477 if key == 'components':
478 self._components[value.get('name')] = value
479
480 self._update_components()
481
482 def atmosphereStateRemove(state, key, value):
483 if key == 'components':
484 self._remove_component(value)
485
486 def planetStateAppend(state, key, value):
487 if key == 'flows':
488 if value != state.get('flows')[0]:
489 self.warning('flumotion-admin can only handle one '
490 'flow, ignoring /%s', value.get('name'))
491 return
492 self.debug('%s flow started', value.get('name'))
493 value.addListener(self, append=flowStateAppend,
494 remove=flowStateRemove)
495 for c in value.get('components'):
496 flowStateAppend(value, 'components', c)
497
498 def planetStateRemove(state, key, value):
499 self.debug('something got removed from the planet')
500
501 def planetStateSetitem(state, key, subkey, value):
502 if key == 'messages':
503 self._messages_view.add_message(value)
504
505 def planetStateDelitem(state, key, subkey, value):
506 if key == 'messages':
507 self._messages_view.clear_message(value.id)
508
509 self.debug('parsing planetState %r' % planetState)
510 self._planetState = planetState
511
512
513 self._components = {}
514
515 planetState.addListener(self, append=planetStateAppend,
516 remove=planetStateRemove,
517 setitem=planetStateSetitem,
518 delitem=planetStateDelitem)
519
520 self._clear_messages()
521
522 a = planetState.get('atmosphere')
523 a.addListener(self, append=atmosphereStateAppend,
524 remove=atmosphereStateRemove)
525 for c in a.get('components'):
526 atmosphereStateAppend(a, 'components', c)
527
528 for f in planetState.get('flows'):
529 planetStateAppend(planetState, 'flows', f)
530
531
532
534 def propertyErrback(failure):
535 failure.trap(errors.PropertyError)
536 self._error("%s." % failure.getErrorMessage())
537 return None
538
539 def after_getProperty(value, dialog):
540 self.debug('got value %r' % value)
541 dialog.update_value_entry(value)
542
543 def dialog_set_cb(dialog, element, property, value, state):
544 cb = self._admin.setProperty(state, element, property, value)
545 cb.addErrback(propertyErrback)
546 def dialog_get_cb(dialog, element, property, state):
547 cb = self._admin.getProperty(state, element, property)
548 cb.addCallback(after_getProperty, dialog)
549 cb.addErrback(propertyErrback)
550
551 name = state.get('name')
552 d = dialogs.PropertyChangeDialog(name, self._window)
553 d.connect('get', dialog_get_cb, state)
554 d.connect('set', dialog_set_cb, state)
555 d.run()
556
572
583
585 """
586 @returns: a L{twisted.internet.defer.Deferred}
587 """
588 return self._component_do(state, 'Stop', 'Stopping', 'Stopped')
589
591 """
592 @returns: a L{twisted.internet.defer.Deferred}
593 """
594 return self._component_do(state, 'Start', 'Starting', 'Started')
595
603
605 """
606 @returns: a L{twisted.internet.defer.Deferred}
607 """
608 return self._component_do(state, '', 'Deleting', 'Deleted',
609 'deleteComponent')
610
611 - def _component_do(self, state, action, doing, done,
612 remoteMethodPrefix="component"):
613 """
614 @param remoteMethodPrefix: prefix for remote method to run
615 """
616 if not state:
617 state = self.components_view.get_selected_state()
618 if not state:
619 self.statusbar.push('main', _("No component selected."))
620 return None
621
622 name = getComponentLabel(state)
623 if not name:
624 return None
625
626 mid = self.statusbar.push('main', "%s component %s" % (doing, name))
627 d = self._admin.callRemote(remoteMethodPrefix + action, state)
628
629 def _actionCallback(result, self, mid):
630 self.statusbar.remove('main', mid)
631 self.statusbar.push('main', "%s component %s" % (done, name))
632 def _actionErrback(failure, self, mid):
633 self.statusbar.remove('main', mid)
634 self.warning("Failed to %s component %s: %s" % (
635 action.lower(), name, failure))
636 self.statusbar.push('main',
637 _("Failed to %(action)s component %(name)s") % {
638 'action': action.lower(),
639 'name': name,
640 })
641
642 d.addCallback(_actionCallback, self, mid)
643 d.addErrback(_actionErrback, self, mid)
644
645 return d
646
648 self.debug('action %s on component %s' % (action,
649 state.get('name')))
650 method_name = '_component_' + action
651 if hasattr(self, method_name):
652 getattr(self, method_name)(state)
653 else:
654 self.warning("No method '%s' implemented" % method_name)
655
661
662 def compAppend(state, key, value):
663 name = state.get('name')
664 self.debug('stateAppend on component state of %s' % name)
665 if key == 'messages':
666 current = self.components_view.get_selected_name()
667 if name == current:
668 self._messages_view.add_message(value)
669
670 def compRemove(state, key, value):
671 name = state.get('name')
672 self.debug('stateRemove on component state of %s' % name)
673 if key == 'messages':
674 current = self.components_view.get_selected_name()
675 if name == current:
676 self._messages_view.clear_message(value.id)
677
678 if self._current_component_state:
679 self._current_component_state.removeListener(self)
680 self._current_component_state = state
681 if self._current_component_state:
682 self._current_component_state.addListener(
683 self, compSet, compAppend, compRemove)
684
685 self._update_component_actions()
686 self._component_view.show_object(state)
687 self._clear_messages()
688
689 if state:
690 name = getComponentLabel(state)
691
692 messages = state.get('messages')
693 if messages:
694 for m in messages:
695 self.debug('have message %r' % m)
696 self._messages_view.add_message(m)
697
698 if state.get('mood') == moods.sad.value:
699 self.debug('component %s is sad' % name)
700 self.statusbar.set('main',
701 _("Component %s is sad") % name)
702
703
704
705
706
707
708
709
710
711
712
713
715 self.info('Connected to manager')
716 if self._disconnected_dialog:
717 self._disconnected_dialog.destroy()
718 self._disconnected_dialog = None
719
720
721 self._window.set_title(_('%s - Flumotion Administration') %
722 self._admin.adminInfoStr())
723 self._trayicon.set_tooltip(self._admin.adminInfoStr())
724
725 self.emit('connected')
726
727 self._component_view.set_single_admin(admin)
728
729 self._set_planet_state(admin.planet)
730
731 if not self._components:
732 self.debug('no components detected, running wizard')
733
734 self.show()
735 self._run_wizard()
736
738 self._components = {}
739 self._update_components()
740 self._clear_messages()
741 if self._planetState:
742 self._planetState.removeListener(self)
743 self._planetState = None
744
745 def response(dialog, id):
746 if id == gtk.RESPONSE_CANCEL:
747
748 dialog.destroy()
749 return
750 elif id == 1:
751 self._admin.reconnect()
752
753 message = _("Lost connection to manager, reconnecting ...")
754 d = gtk.MessageDialog(self._window, gtk.DIALOG_DESTROY_WITH_PARENT,
755 gtk.MESSAGE_WARNING, gtk.BUTTONS_NONE, message)
756
757 RESPONSE_REFRESH = 1
758 d.add_button(gtk.STOCK_REFRESH, RESPONSE_REFRESH)
759 d.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
760 d.connect("response", response)
761 d.show_all()
762 self._disconnected_dialog = d
763
774
775 log.debug('adminclient', "handling connection-refused")
776 reactor.callLater(0, refused_later)
777 log.debug('adminclient', "handled connection-refused")
778
792
793 log.debug('adminclient', "handling connection-failed")
794 reactor.callLater(0, failed_later)
795 log.debug('adminclient', "handled connection-failed")
796
801
808
809 d.connect('have-connection', on_have_connection)
810 d.show()
811
826
827 def cancel(failure):
828 failure.trap(WizardCancelled)
829 wiz.stop()
830
831 d = wiz.run_async()
832 d.addCallback(got_state, wiz)
833 d.addErrback(cancel)
834
836 d = gtk.FileChooserDialog(_("Import Configuration..."), self._window,
837 gtk.FILE_CHOOSER_ACTION_OPEN,
838 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
839 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
840 d.set_default_response(gtk.RESPONSE_ACCEPT)
841
842 def response(d, response):
843 if response == gtk.RESPONSE_ACCEPT:
844 name = d.get_filename()
845 conf_xml = open(name, 'r').read()
846 self._admin.loadConfiguration(conf_xml)
847 d.destroy()
848
849 d.connect('response', response)
850 d.show()
851
853 d = gtk.FileChooserDialog(_("Export Configuration..."), self._window,
854 gtk.FILE_CHOOSER_ACTION_SAVE,
855 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
856 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
857 d.set_default_response(gtk.RESPONSE_ACCEPT)
858
859 def get_configuration(conf_xml, name, chooser):
860 file_exists = True
861 if os.path.exists(name):
862 d = gtk.MessageDialog(self._window, gtk.DIALOG_MODAL,
863 gtk.MESSAGE_ERROR, gtk.BUTTONS_YES_NO,
864 _("File already exists.\nOverwrite?"))
865 d.connect("response", lambda self, response: d.hide())
866 if d.run() == gtk.RESPONSE_YES:
867 file_exists = False
868 else:
869 file_exists = False
870
871 if not file_exists:
872 f = open(name, 'w')
873 f.write(conf_xml)
874 f.close()
875 chooser.destroy()
876
877 def response(d, response):
878 if response == gtk.RESPONSE_ACCEPT:
879 deferred = self._admin.getConfiguration()
880 name = d.get_filename()
881 deferred.addCallback(get_configuration, name, d)
882 else:
883 d.destroy()
884
885 d.connect('response', response)
886 d.show()
887
889 return self._admin.callRemote('reloadManager')
890
892 self.info('Reloading admin code')
893 from flumotion.common.reload import reload as freload
894 freload()
895 self.info('Reloaded admin code')
896
898 dialog = dialogs.ProgressDialog(_("Reloading ..."),
899 _("Reloading client code"), self._window)
900
901
902 def _stopCallback(result):
903 dialog.stop()
904 dialog.destroy()
905
906 def _syntaxErrback(failure):
907 failure.trap(errors.ReloadSyntaxError)
908 dialog.stop()
909 dialog.destroy()
910 self._error(
911 _("Could not reload component:\n%s.") %
912 failure.getErrorMessage())
913 return None
914
915 def _defaultErrback(failure):
916 self.warning('Errback: unhandled failure: %s' %
917 failure.getErrorMessage())
918 return failure
919
920 def _callLater():
921 d = maybeDeferred(self._reload_admin)
922 d.addCallback(lambda _: self._reload_manager())
923
924
925 for c in self._components.values():
926
927 d.addCallback(lambda _, c: self._component_reload(c), c)
928 d.addCallback(_stopCallback)
929 d.addErrback(_syntaxErrback)
930 d.addErrback(_defaultErrback)
931
932
933 def _reloadCallback(admin, text):
934 dialog.message(_("Reloading %s code") % text)
935
936 self._admin.connect('reloading', _reloadCallback)
937 dialog.start()
938 reactor.callLater(0.2, _callLater)
939
941 if sys.version_info >= (2, 4):
942 from flumotion.extern import code
943 else:
944 import code
945
946 vars = \
947 {
948 "admin": self._admin,
949 "components": self._components
950 }
951 message = """Flumotion Admin Debug Shell
952
953 Local variables are:
954 admin (flumotion.admin.admin.AdminModel)
955 components (dict: name -> flumotion.common.planet.AdminComponentState)
956
957 You can do remote component calls using:
958 admin.componentCallRemote(components['component-name'],
959 'methodName', arg1, arg2)
960
961 """
962 code.interact(local=vars, banner=message)
963
968
969
970
973
976
979
982
985
986
987
990
993
996
999
1002
1003
1004
1007
1010
1013
1016
1019
1022
1025
1028
1032
1036
1039
1042
1045
1048
1051
1054
1057
1058 pygobject.type_register(Window)
1059