1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """main interface for the cursor admin client"""
23
24 import curses
25 import os
26 import string
27
28 import gobject
29 from twisted.internet import reactor
30 from twisted.python import rebuild
31 from zope.interface import implements
32
33 from flumotion.common import log, errors, common
34 from flumotion.twisted import flavors, reflect
35 from flumotion.common.planet import moods
36
37 from flumotion.admin.text import misc_curses
38
39 __version__ = "$Rev: 6990 $"
40
41
42 -class AdminTextView(log.Loggable, gobject.GObject, misc_curses.CursesStdIO):
43
44 implements(flavors.IStateListener)
45
46 logCategory = 'admintextview'
47
48 global_commands = [ 'startall', 'stopall', 'clearall', 'quit' ]
49
50 LINES_BEFORE_COMPONENTS = 5
51 LINES_AFTER_COMPONENTS = 6
52
53 - def __init__(self, model, stdscr):
54 self.initialised = False
55 self.stdscr = stdscr
56 self.inputText = ''
57 self.command_result = ""
58 self.lastcommands = []
59 self.nextcommands = []
60 self.rows, self.cols = self.stdscr.getmaxyx()
61 self.max_components_per_page = self.rows - \
62 self.LINES_BEFORE_COMPONENTS - \
63 self.LINES_AFTER_COMPONENTS
64 self._first_onscreen_component = 0
65
66 self._components = {}
67 self._comptextui = {}
68 self._setAdminModel(model)
69
70 self.setPlanetState(self.admin.planet)
71
72
73 - def _setAdminModel(self, model):
74 self.admin = model
75
76 self.admin.connect('connected', self.admin_connected_cb)
77 self.admin.connect('disconnected', self.admin_disconnected_cb)
78 self.admin.connect('connection-refused',
79 self.admin_connection_refused_cb)
80 self.admin.connect('connection-failed',
81 self.admin_connection_failed_cb)
82
83
84 self.admin.connect('update', self.admin_update_cb)
85
86
88 self.initialised = True
89 self.stdscr.addstr(0,0, "Main Menu")
90 self.show_components()
91 self.display_status()
92 self.stdscr.move(self.lasty,0)
93 self.stdscr.clrtoeol()
94 self.stdscr.move(self.lasty+1,0)
95 self.stdscr.clrtoeol()
96 self.stdscr.addstr(self.lasty+1,0, "Prompt: %s" % self.inputText)
97 self.stdscr.refresh()
98
99
100
101
102 - def show_components(self):
103 if self.initialised:
104 self.stdscr.addstr(2,0, "Components:")
105
106 names = self._components.keys()
107 names.sort()
108
109 cury = 4
110
111
112
113
114 if len(names) > self.max_components_per_page:
115 if self._first_onscreen_component > 0:
116 self.stdscr.move(cury,0)
117 self.stdscr.clrtoeol()
118 self.stdscr.addstr(cury,0,
119 "Press page up to scroll up components list")
120 cury=cury+1
121 cur_component = self._first_onscreen_component
122 for name in names[self._first_onscreen_component:len(names)]:
123
124 if cury - self.LINES_BEFORE_COMPONENTS >= \
125 self.max_components_per_page:
126 self.stdscr.move(cury,0)
127 self.stdscr.clrtoeol()
128 self.stdscr.addstr(cury,0,
129 "Press page down to scroll down components list")
130 cury = cury + 1
131 break
132
133 component = self._components[name]
134 mood = component.get('mood')
135
136 self.stdscr.move(cury,0)
137 self.stdscr.clrtoeol()
138
139 self.stdscr.addstr(cury,0,"%s: %s" % (name, moods[mood].name))
140 cury = cury + 1
141 cur_component = cur_component + 1
142
143 self.lasty = cury
144
145
146
147 - def gotEntryCallback(self, result, name):
148 entryPath, filename, methodName = result
149 filepath = os.path.join(entryPath, filename)
150 self.debug('Got the UI for %s and it lives in %s' % (name,filepath))
151 self.uidir = os.path.split(filepath)[0]
152
153
154
155
156
157 moduleName = common.pathToModuleName(filename)
158 statement = 'import %s' % moduleName
159 self.debug('running %s' % statement)
160 try:
161 exec(statement)
162 except SyntaxError, e:
163
164 where = getattr(e, 'filename', "<entry file>")
165 lineno = getattr(e, 'lineno', 0)
166 msg = "Syntax Error at %s:%d while executing %s" % (
167 where, lineno, filename)
168 self.warning(msg)
169 raise errors.EntrySyntaxError(msg)
170 except NameError, e:
171
172 msg = "NameError while executing %s: %s" % (filename,
173 " ".join(e.args))
174 self.warning(msg)
175 raise errors.EntrySyntaxError(msg)
176 except ImportError, e:
177 msg = "ImportError while executing %s: %s" % (filename,
178 " ".join(e.args))
179 self.warning(msg)
180 raise errors.EntrySyntaxError(msg)
181
182
183 module = reflect.namedAny(moduleName)
184 rebuild.rebuild(module)
185
186
187 if not hasattr(module, methodName):
188 self.warning('method %s not found in file %s' % (
189 methodName, filename))
190 raise
191 klass = getattr(module, methodName)
192
193
194
195
196 instance = klass(self._components[name], self.admin)
197 self.debug("Created entry instance %r" % instance)
198
199
200
201 self._comptextui[name] = instance
202
203
204 - def gotEntryNoBundleErrback(self, failure, name):
205 failure.trap(errors.NoBundleError)
206 self.debug("No admin ui for component %s" % name)
207
210
211 - def getEntry(self, componentState, type):
212 """
213 Do everything needed to set up the entry point for the given
214 component and type, including transferring and setting up bundles.
215
216 Caller is responsible for adding errbacks to the deferred.
217
218 @returns: a deferred returning (entryPath, filename, methodName) with
219 entryPath: the full local path to the bundle's base
220 fileName: the relative location of the bundled file
221 methodName: the method to instantiate with
222 """
223 lexicalVariableHack = []
224
225 def gotEntry(res):
226 fileName, methodName = res
227 lexicalVariableHack.append(res)
228 self.debug("entry for %r of type %s is in file %s and method %s",
229 componentState, type, fileName, methodName)
230 return self.admin.bundleLoader.getBundles(fileName=fileName)
231
232 def gotBundles(res):
233 name, bundlePath = res[-1]
234 fileName, methodName = lexicalVariableHack[0]
235 return (bundlePath, fileName, methodName)
236
237 d = self.admin.callRemote('getEntryByType',
238 componentState.get('type'), type)
239 d.addCallback(gotEntry)
240 d.addCallback(gotBundles)
241 return d
242
243 - def update_components(self, components):
244 for name in self._components.keys():
245 component = self._components[name]
246 try:
247 component.removeListener(self)
248 except KeyError:
249
250 self.debug("silly")
251
252 def compStateSet(state, key, value):
253 self.log('stateSet: state %r, key %s, value %r' % (state, key, value))
254
255 if key == 'mood':
256
257
258 d = self.getEntry(state, 'admin/text')
259 d.addCallback(self.gotEntryCallback, state.get('name'))
260 d.addErrback(self.gotEntryNoBundleErrback, state.get('name'))
261 d.addErrback(self.gotEntrySleepingComponentErrback)
262
263 self.show()
264 elif key == 'name':
265 if value:
266 self.show()
267
268 self._components = components
269 for name in self._components.keys():
270 component = self._components[name]
271 component.addListener(self, set_=compStateSet)
272
273
274 d = self.getEntry(component, 'admin/text')
275 d.addCallback(self.gotEntryCallback, name)
276 d.addErrback(self.gotEntryNoBundleErrback, name)
277 d.addErrback(self.gotEntrySleepingComponentErrback)
278
279 self.show()
280
281 - def setPlanetState(self, planetState):
282 def flowStateAppend(state, key, value):
283 self.debug('flow state append: key %s, value %r' % (key, value))
284 if state.get('name') != 'default':
285 return
286 if key == 'components':
287 self._components[value.get('name')] = value
288
289 self.update_components(self._components)
290
291 def flowStateRemove(state, key, value):
292 if state.get('name') != 'default':
293 return
294 if key == 'components':
295 name = value.get('name')
296 self.debug('removing component %s' % name)
297 del self._components[name]
298
299 self.update_components(self._components)
300
301 def atmosphereStateAppend(state, key, value):
302 if key == 'components':
303 self._components[value.get('name')] = value
304
305 self.update_components(self._components)
306
307 def atmosphereStateRemove(state, key, value):
308 if key == 'components':
309 name = value.get('name')
310 self.debug('removing component %s' % name)
311 del self._components[name]
312
313 self.update_components(self._components)
314
315 def planetStateAppend(state, key, value):
316 if key == 'flows':
317 if value.get('name') != 'default':
318 return
319
320 value.addListener(self, append=flowStateAppend,
321 remove=flowStateRemove)
322 for c in value.get('components'):
323 flowStateAppend(value, 'components', c)
324
325 def planetStateRemove(state, key, value):
326 self.debug('something got removed from the planet')
327
328 self.debug('parsing planetState %r' % planetState)
329 self._planetState = planetState
330
331
332 self._components = {}
333
334 planetState.addListener(self, append=planetStateAppend,
335 remove=planetStateRemove)
336
337 a = planetState.get('atmosphere')
338 a.addListener(self, append=atmosphereStateAppend,
339 remove=atmosphereStateRemove)
340 for c in a.get('components'):
341 atmosphereStateAppend(a, 'components', c)
342
343 for f in planetState.get('flows'):
344 planetStateAppend(f, 'flows', f)
345
346 - def _component_stop(self, state):
347 return self._component_do(state, 'Stop', 'Stopping', 'Stopped')
348
349 - def _component_start(self, state):
350 return self._component_do(state, 'Start', 'Starting', 'Started')
351
352 - def _component_do(self, state, action, doing, done):
353 name = state.get('name')
354 if not name:
355 return None
356
357 self.admin.callRemote('component'+action, state)
358
359 - def run_command(self, command):
360
361 can_stop = True
362 can_start = True
363 for x in self._components.values():
364 mood = moods.get(x.get('mood'))
365 can_stop = can_stop and (mood != moods.lost and mood != moods.sleeping)
366 can_start = can_start and (mood == moods.sleeping)
367 can_clear = can_start and not can_stop
368
369 if string.lower(command) == 'quit':
370 reactor.stop()
371 elif string.lower(command) == 'startall':
372 if can_start:
373 for c in self._components.values():
374 self._component_start(c)
375 self.command_result = 'Attempting to start all components'
376 else:
377 self.command_result = 'Components not all in state to be started'
378
379
380 elif string.lower(command) == 'stopall':
381 if can_stop:
382 for c in self._components.values():
383 self._component_stop(c)
384 self.command_result = 'Attempting to stop all components'
385 else:
386 self.command_result = 'Components not all in state to be stopped'
387 elif string.lower(command) == 'clearall':
388 if can_clear:
389 self.admin.cleanComponents()
390 self.command_result = 'Attempting to clear all components'
391 else:
392 self.command_result = 'Components not all in state to be cleared'
393 else:
394 command_split = command.split()
395
396 if len(command_split)>1:
397
398 for c in self._components.values():
399 if string.lower(c.get('name')) == string.lower(command_split[0]):
400
401 if string.lower(command_split[1]) == 'start':
402
403 self._component_start(c)
404 elif string.lower(command_split[1]) == 'stop':
405
406 self._component_stop(c)
407 else:
408
409 try:
410 textui = self._comptextui[c.get('name')]
411
412 if textui:
413 d = textui.runCommand(' '.join(command_split[1:]))
414 self.debug("textui runcommand defer: %r" % d)
415
416 d.addCallback(self._runCommand_cb)
417
418 except KeyError:
419 pass
420
421
422 - def _runCommand_cb(self, result):
423 self.command_result = result
424 self.debug("Result received: %s" % result)
425 self.show()
426
427
428
429
430 - def get_available_commands(self, input):
431 input_split = input.split()
432 last_input=''
433 if len(input_split) >0:
434 last_input = input_split[len(input_split)-1]
435 available_commands = []
436 if len(input_split) <= 1 and not input.endswith(' '):
437
438 can_stop = True
439 can_start = True
440 for x in self._components.values():
441 mood = moods.get(x.get('mood'))
442 can_stop = can_stop and (mood != moods.lost and mood != moods.sleeping)
443 can_start = can_start and (mood == moods.sleeping)
444 can_clear = can_start and not can_stop
445
446 for command in self.global_commands:
447 command_ok = (command != 'startall' and command != 'stopall' and command != 'clearall')
448 command_ok = command_ok or (command == 'startall' and can_start)
449 command_ok = command_ok or (command == 'stopall' and can_stop)
450 command_ok = command_ok or (command == 'clearall' and can_clear)
451
452 if command_ok and string.lower(command).startswith(string.lower(last_input)):
453 available_commands.append(command)
454 else:
455 available_commands = available_commands + self.get_available_commands_for_component(input_split[0], input)
456
457 return available_commands
458
460 self.debug("getting commands for component %s" % comp)
461 commands = []
462 for c in self._components:
463 if c == comp:
464 component_commands = [ 'start', 'stop' ]
465 textui = None
466 try:
467 textui = self._comptextui[comp]
468 except KeyError:
469 self.debug("no text ui for component %s" % comp)
470
471 input_split = input.split()
472
473 if len(input_split) >= 2 or input.endswith(' '):
474 for command in component_commands:
475 if len(input_split) == 2:
476 if command.startswith(input_split[1]):
477 commands.append(command)
478 elif len(input_split) == 1:
479 commands.append(command)
480 if textui:
481 self.debug("getting component commands from ui of %s" % comp)
482 comp_input = ' '.join(input_split[1:])
483 if input.endswith(' '):
484 comp_input = comp_input + ' '
485 commands = commands + textui.getCompletions(comp_input)
486
487 return commands
488
489
491 completions = self.get_available_commands(input)
492
493
494 if len(input.split()) <= 1:
495 for c in self._components:
496 if c.startswith(input):
497 completions.append(c)
498
499 return completions
500
501 - def display_status(self):
502 availablecommands = self.get_available_commands(self.inputText)
503 available_commands = ' '.join(availablecommands)
504
505
506 self.stdscr.move(self.lasty+2,0)
507 self.stdscr.clrtoeol()
508
509 self.stdscr.addstr(self.lasty+2, 0,
510 "Available Commands: %s" % available_commands)
511
512 self.stdscr.move(self.lasty+3,0)
513 self.stdscr.clrtoeol()
514 self.stdscr.move(self.lasty+4,0)
515 self.stdscr.clrtoeol()
516
517 if self.command_result != "":
518 self.stdscr.addstr(self.lasty+4, 0, "Result: %s" % self.command_result)
519 self.stdscr.clrtobot()
520
521
522 - def admin_connected_cb(self, admin):
523 self.info('Connected to manager')
524
525
526 self.setPlanetState(self.admin.planet)
527
528 if not self._components:
529 self.debug('no components detected, running wizard')
530
531 self.show()
532
533 - def admin_disconnected_cb(self, admin):
534 message = "Lost connection to manager, reconnecting ..."
535 print message
536
538 log.debug('textadminclient', "handling connection-refused")
539
540 log.debug('textadminclient', "handled connection-refused")
541
543 log.debug('textadminclient', "handling connection-failed")
544
545 log.debug('textadminclient', "handled connection-failed")
546
547 - def admin_update_cb(self, admin):
548 self.update_components(self._components)
549
550 - def connectionLost(self, why):
553
554 - def whsStateAppend(self, state, key, value):
555 if key == 'names':
556 self.debug('Worker %s logged in.' % value)
557
558 - def whsStateRemove(self, state, key, value):
559 if key == 'names':
560 self.debug('Worker %s logged out.' % value)
561
562
564 """ Input is ready! """
565 c = self.stdscr.getch()
566
567 if c == curses.KEY_BACKSPACE or c == 127:
568 self.inputText = self.inputText[:-1]
569 elif c == curses.KEY_STAB or c == 9:
570 available_commands = self.get_available_completions(self.inputText)
571 if len(available_commands) == 1:
572 input_split = self.inputText.split()
573 if len(input_split) > 1:
574 if not self.inputText.endswith(' '):
575 input_split.pop()
576 self.inputText = ' '.join(input_split) + ' ' + available_commands[0]
577 else:
578 self.inputText = available_commands[0]
579
580 elif c == curses.KEY_ENTER or c == 10:
581
582 self.run_command(self.inputText)
583
584 self.display_status()
585
586 self.stdscr.move(self.lasty+1,0)
587 self.stdscr.clrtoeol()
588 self.stdscr.addstr(self.lasty+1,0,'Prompt: ')
589 self.stdscr.refresh()
590 if len(self.nextcommands) > 0:
591 self.lastcommands = self.lastcommands + self.nextcommands
592 self.nextcommands = []
593 self.lastcommands.append(self.inputText)
594 self.inputText = ''
595 self.command_result = ''
596 elif c == curses.KEY_UP:
597 lastcommand = ""
598 if len(self.lastcommands) > 0:
599 lastcommand = self.lastcommands.pop()
600 if self.inputText != "":
601 self.nextcommands.append(self.inputText)
602 self.inputText = lastcommand
603 elif c == curses.KEY_DOWN:
604 nextcommand = ""
605 if len(self.nextcommands) > 0:
606 nextcommand = self.nextcommands.pop()
607 if self.inputText != "":
608 self.lastcommands.append(self.inputText)
609 self.inputText = nextcommand
610 elif c == curses.KEY_PPAGE:
611 if self._first_onscreen_component > 0:
612 self._first_onscreen_component = \
613 self._first_onscreen_component - 1
614 self.show()
615 elif c == curses.KEY_NPAGE:
616 if self._first_onscreen_component < len(self._components) - \
617 self.max_components_per_page:
618 self._first_onscreen_component = \
619 self._first_onscreen_component + 1
620 self.show()
621
622 else:
623
624 if len(self.inputText) == self.cols-2: return
625
626 if c<=256:
627 self.inputText = self.inputText + chr(c)
628
629
630 self.display_status()
631
632 self.stdscr.move(self.lasty+1,0)
633 self.stdscr.clrtoeol()
634
635 self.stdscr.addstr(self.lasty+1, 0, 'Prompt: %s' % self.inputText)
636 self.stdscr.refresh()
637
638
639
640
641 - def componentCall(self, componentState, methodName, *args, **kwargs):
642
643
644
645
646 self.log("componentCall received for %r.%s ..." % (
647 componentState, methodName))
648 localMethodName = "component_%s" % methodName
649 name = componentState.get('name')
650
651 try:
652 textui = self._comptextui[name]
653 except KeyError:
654 return
655
656 if not hasattr(textui, localMethodName):
657 self.log("... but does not have method %s" % localMethodName)
658 self.warning("Component view %s does not implement %s" % (
659 name, localMethodName))
660 return
661 self.log("... and executing")
662 method = getattr(textui, localMethodName)
663
664
665 try:
666 result = method(*args, **kwargs)
667 except TypeError:
668 msg = "component method %s did not accept *a %s and **kwa %s (or TypeError)" % (
669 methodName, args, kwargs)
670 self.debug(msg)
671 raise errors.RemoteRunError(msg)
672 self.log("component: returning result: %r to caller" % result)
673 return result
674