#!/usr/bin/env python # Thu Dec 9 20:24:33 PST 2004 # Tossing my "ssh-grid" prototype. It's great for small numbers of hosts, but # it's going to fall apart with more than a few 10's of hosts or 10's of # commands, even with abbreviations. Also, ssh-grid started out too # procedurally - this is a large enough project that a more OOP approach is # almost certainly going to prove highly beneficial # future plans (from ssh-grid, but mostly applies here too) : # # 1) Need a "kill" option, EG For mozilla's and jpilot's that crash. # That's just another command. Already done effectively for evolution. # # 2) Need to be able to specify a long hostname and a short hostname, # defaulting to the obvious. Done. # # 3) Need to sort out why ssh X tunneling is unreliable with FC[23]! Done. # # 4) Need to be able to specify a short name for a command, as well as # the full command! EG, some commands want to specify a PATH or # something... That shouldn't clutter up the GUI. Done. # # 5) It'd be really sweet to have some sort of "recent pairings" pane! # # 6) Continue work on "multiple" option. Done. # # 7) Add some explanatory tooltips. Done. # # 8) Make buttons more persistent. Done. # # 9) Add a "submit" button for multiple option. Done. # # 10) Grey out some things? There are obvious things, like greying out # the submit button when multiple isn't in effect, but what about the # possibility of checking if various commands are present on a system, # and greying them (perhaps with an override) if they don't show up on a # particular path? That'd be really cool. # # 11) Look into why "mozilla -dont" isn't flying. Is it a "two word" # problem in deep-ssh, or is it just some sort of x11 forwarding thing I # was seeing? Done. # # 12) what about "worker" and "xfe" (or was it "xce"?)? Are they worth # copying around? Is a generic "propagate this to target system" option # worthwhile? Probably best done as "just another command". # # 13) file transfers? -Now- wer're starting to get ambitious. :) # Maybe sftpserv and/or xcb? Or both :) sftpserv is more likely to be # present on your random system though. # # 14) Maybe make deep-ssh into a python module... Or simply merge it... # # 15) Maybe have ssh-grid - like functionality if the user so chooses... # There may be other alternative interfaces that'd be nice too. # (ssh-grid was just a matrix of buttons, with the (EG) rows being # labeled with hostnames, and the columns being labeled with commands. # It was great for small numbers of each, but grew unwieldy much more # quickly than sshgui's interface.) # # 16) Change stderr errors to a dialog. Done. # # 17) Rewrite? There's a -Lot- of potential for using inheritance in # this application, which I really haven't capitalized on as yet... In # fact, while some of the code is heavily parameterized (but still no # inheritance), other portions are just cut-and-paste-and-substitutes... import pygtk pygtk.require('2.0') import gtk import os import string import sys import time import copy class hostgroup_class: def __init__(self,name,hosts): self.name = name self.hosts = hosts def tempfn(): return '/tmp/sshgui-%d-%d' % (os.getpid(), int(time.time() * 10000)) class commandgroup_class: def __init__(self,name,commands): self.name = name self.commands = commands class hostpath_class: def __init__(self,path,shortname,group): # "path" may be a hostname, or a bang path of hostnames - used for actual # execution of commands self.path = path self.pathlist = string.splitfields(self.path,'!') # shortname is the representation of the path in the GUI # this should be done on the fly - not memorized if shortname == '': list = [] for host in self.pathlist: f = string.splitfields(host,'.') if len(f) == 1: list.append(host) else: list.append(f[0]) self.shortname = string.joinfields(list,'!') else: self.shortname = shortname # revpathlist is for doing hostpath comparisons - IE, sorting self.revpathlist = string.splitfields(self.shortname) self.revpathlist.reverse() for i in range(len(self.revpathlist)): f = string.splitfields(self.revpathlist[i],'@') if len(f) == 1: # nothing to change pass elif len(f) == 2: self.revpathlist[i] = f[1] else: self.warn('Strange number of @ signs in hostpath: %s\nUsing %s' % (self.revpathlist[i],f[1])) self.revpathlist[i] = f[1] # What kind of host is this? Used for grouping self.group = group def __cmp__(self,other): l1 = len(self.revpathlist) l2 = len(other.revpathlist) m = min(l1,l2) for i in range(m): if self.revpathlist[i] < other.revpathlist[i]: #print self.revpathlist[i],'<',other.revpathlist[i] return -1 elif self.revpathlist[i] > other.revpathlist[i]: #print self.revpathlist[i],'>',other.revpathlist[i] return 1 # equal to this point if l1 < l2: # l1 is shorter, so it's less #print l1,'<',l2 return -1 elif l1 > l2: # l1 is longer, so it's more #print l1,'>',l2 return 1 else: # lists are identical, result is equal #print l1,'==',l2 return 0 def __repr__(self): return self.path def __str__(self): return self.shortname def write(self,file): file.write(self.path+'\0'+self.shortname+'\0'+self.group+'\0') class command_class: def __init__(self,command,shortname,group,rununder): # "path" is the full command, along with with any $PATH specification self.command = command # shortname is the representation of the path in the GUI if shortname == '': # this should do some form of abbreviation - and it shouldn't # memorize it for all time, either self.shortname = self.command else: self.shortname = shortname # What kind of command is this? Used for grouping self.group = group # What command do we run this under? self.rununder = rununder def __cmp__(self,other): if self.shortname < other.shortname: return -1 elif self.shortname > other.shortname: return 1 else: return 0 def __repr__(self): return self.command def __str__(self): return self.shortname def write(self,file): file.write(self.command+'\0'+self.shortname+'\0'+self.group+'\0'+ \ self.rununder+'\0') # this eliminated a lot a redundancy, in a way I've never seen a language able # to before. :) Maybe smalltalk could too though. def read_it(fn,claz,fields_should_be): # needs locking! sys.stderr.write('read_it needs locking!\n') n = len(fields_should_be) filename=os.path.expanduser(fn) try: os.stat(filename) except: file = open(filename,'w') file.write(str(len(fields_should_be))+'\0') for field in fields_should_be: file.write(field+'\0') file.close() try: file = open(filename,'r') except: self.error('Could not open %s\n' % filename) try: content=file.read() except: self.error('Could not read %s\n' % filename) try: file.close() except: self.error('Could not close %s\n' % filename) fields = string.splitfields(content,'\0') try: fields_per_record = string.atoi(fields[0]) except: self.error('First field of %s must be the ASCII number %s\n' % n) if fields_per_record != n: self.error('First field of %s must be the ASCII number %s\n' % n) del fields[0] for fieldno in range(fields_per_record): if fields[fieldno] != fields_should_be[fieldno]: self.error("Field %d of %s must be %s\n" % \ (fieldno, filename, fields_should_be[fieldno])) # OK, we have the right number of fields in a record, and the field names # are correct # # Now see if we have the right number of records! len_of_fields = len(fields) records = len_of_fields / fields_per_record if records * fields_per_record + 1 != len_of_fields: self.error("Incorrect number of fields in %s\n" % filename) # some editors will want to put a newline at the end of the file. Cope. if fields[-1] != '' and fields[-1] != '\n': self.error("Last record of %s should have a single empty field\n" % fn) del fields[-1] groups = {} # skip the first "record", because it's just a decription of the file # format - IE, that's why we start at 1, instead of 0 for recordno in range(1,records): instance = apply(claz, \ fields[recordno*fields_per_record:(recordno+1)*fields_per_record]) if groups.has_key(instance.group): groups[instance.group].append(instance) else: groups[instance.group] = [instance] return groups def write_it(fn,groups,group,fields): # needs locking! sys.stderr.write('write_it needs locking!\n') n = len(fields) filename=os.path.expanduser(fn) try: file = open(filename+'.tmp','w') except: self.error('Could not open %s\n' % filename) file.write(str(len(fields))+'\0') for field in fields: file.write(field+'\0') # this is so beautifully indirect! :) for grp in groups.keys(): #print 'grp is',grp #print 'groups is',groups #print 'field group is',getattr(groups[grp],group) for item in getattr(groups[grp],group): for field in fields: file.write(getattr(item,field)+'\0') file.close() os.rename(filename+'.tmp',filename) class GUI: def delete_command(self,command_to_del): found = 0 for commandgroup in self.command_groups.keys(): for commandno in range(len(self.command_groups[commandgroup].commands)): command = self.command_groups[commandgroup].commands[commandno] if command_to_del == command: del self.command_groups[commandgroup].commands[commandno] found += 1 return found # untested! Thu Dec 30 22:02:40 PST 2004 # # beginning testing, Sat Jan 1 10:58:29 PST 2005 # mostly working, but fails to remove the hostgroup if it goes empty. Sat Jan 1 11:22:24 PST 2005 # # Getting closer. The hostgroup is deleted now, but in moving a host # from one group to another, there is an empty position in the # destination hostgroup. Sun Jan 2 11:25:02 PST 2005 def delete_host(self,host_to_del): found = 0 for hostgroup in self.host_groups.keys(): del_list = [] for hostno in range(len(self.host_groups[hostgroup].hosts)): host = self.host_groups[hostgroup].hosts[hostno] if host_to_del == host: del_list.append(hostno) #del self.host_groups[hostgroup].hosts[hostno] found += 1 # delete the hosts in reverse order del_list.reverse() for to_del in del_list: del self.host_groups[hostgroup].hosts[to_del] if len(self.host_groups[hostgroup].hosts) == 0: # the hostgroup has 0 hosts in it now, delete the hostgroup # too del self.host_groups[hostgroup] return found def execute_callback(self, widget, data=None): # unconditionally :) But there's an error-check in do_run self.do_run() def deselect_callback(self, widget, data=None): #print 'deselect_callback' self.deselect_all() def do_nothing(self, widget, data=None): pass def about_callback(self, widget, data=None): dia = gtk.Dialog('About sshgui', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123)) dia.set_size_request(500, 300) for line in [ \ 'sshgui is by Dan Stromberg', \ 'You are looking at version '+str(self.version), \ 'http://stromberg.dnsalias.org/~strombrg/sshgui/', \ 'Please see the FAQ at http://stromberg.dnsalias.org/~strombrg/sshgui/FAQ.html', \ 'strombrg@gmail.com' ]: dia.vbox.add(gtk.Label(line)) dia.show_all() result = dia.run() if result == 123: dia.destroy() return 0 else: # hm, that's weird... dia.destroy() self.error('Weird return from dialog in about_callback') def error(self, message): dia = gtk.Dialog('sshgui error!', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123)) dia.vbox.add(gtk.Label(message)) dia.show_all() result = dia.run() if result == 123: dia.destroy() sys.exit(1) else: # hm, that's weird... dia.destroy() sys.stderr.write('Weird return from dialog in error()') sys.exit(1) def warn(self, message): dia = gtk.Dialog('sshgui warning', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123)) dia.vbox.add(gtk.Label(message)) dia.show_all() result = dia.run() if result == 123: dia.destroy() return 0 else: # hm, that's weird... dia.destroy() sys.stderr.write('Weird return from dialog in warn()') sys.exit(1) def help_callback(self, widget, data=None): dia = gtk.Dialog('sshgui help', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123)) dia.set_size_request(300, 300) for line in [ \ 'If you want to run one command on one host, then either click', \ 'the host followed by the command, or vice-versa.', \ ' ', \ 'If you want to run n>=2 commands on one host, then click all', \ 'the commands you want, followed by the single host you want.', \ ' ', \ 'If you want to run 1 command on m>=2 hosts, then click all', \ 'the hosts you want, followed by the single command you want.', \ ' ', \ 'If you want to run n>=2 commands on m>=2 hosts, then first', 'click the "Deferred" button, then click your commands and', 'hosts in any order. Finally, click the "Execute" button.', ' ', \ 'To toggle the selection of an entire host group or command', 'group at a time, click the button for the group.', ' ', \ 'The carets ("^"), at least for now, are used as a reconfigure', \ 'button.', \ ]: dia.vbox.add(gtk.Label(line)) dia.show_all() result = dia.run() if result == 123: dia.destroy() return 0 else: # hm, that's weird... dia.destroy() self.error('Weird return from dialog in help_callback') # returns true if we need to write something, false otherwise def misc_config(self, title, data, fields, del_button,del_fn=None): # assumes everything's a string! if del_button: tuple=("Cancel", 123, "Save", 789, "Delete", 135) else: tuple=("Cancel", 123, "Save", 789) dia = gtk.Dialog(title, self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, tuple) dia.vbox.pack_start(gtk.Label(title)) lenfields = len(fields) table = gtk.Table(2,lenfields,False) entries = [] for fieldno in range(lenfields): table.attach(gtk.Label(fields[fieldno]),0,1,fieldno,fieldno+1) entry = gtk.Entry(256) entry.set_text(getattr(data,fields[fieldno])) entries.append(entry) table.attach(entry,1,2,fieldno,fieldno+1) dia.vbox.pack_start(table) dia.show_all() result = dia.run() if result == 123: # just destroy the dialog dia.destroy() return 0 elif result == 456: # note that this has been disabled... # save results in memory only, then destroy the dialog for fieldno in range(lenfields): setattr(data,entries[fieldno].get_text()) dia.destroy() return 0 elif result == 789: # save results in memory and on disk, then destroy the dialog for fieldno in range(lenfields): setattr(data,fields[fieldno],entries[fieldno].get_text()) # save the changes! #sys.stderr.write('Warning: not saving changes yet!') dia.destroy() return 1 elif result == 135: # save results in memory and on disk, then destroy the dialog for fieldno in range(lenfields): setattr(data,fields[fieldno],entries[fieldno].get_text()) # save the changes! #sys.stderr.write('Warning: not saving changes yet!') dia.destroy() #print del_fn del_result = apply(del_fn,[data]) if del_result != 1: self.warn('Deleted '+str(del_result)+' items') self.draw() return 1 else: # hm, that's weird... dia.destroy() self.error('Weird return from dialog in misc_config') def write_hosts(self): write_it('~/.sshgui/hosts', self.host_groups, 'hosts', ['path', 'shortname', 'group']) def write_commands(self): write_it('~/.sshgui/commands', self.command_groups, 'commands', ['command', 'shortname', 'group', 'rununder']) # there is a Lot of similarity between add_command_callback and # add_host_callback... Maybe split out the common parts into an # add_it? def add_command_callback(self, widget, data=None): #print 'add command' data = command_class('','','','') #print 'before' #self.dump_command_groups() if self.misc_config('Add command ', data, ['command', 'shortname', 'group', 'rununder'], 0): command_found = 0 command_group_found = 0 for commandgroup in self.command_groups.keys(): if data.group == commandgroup: command_group_found = 1 for commandno in range(len(self.command_groups[commandgroup].commands)): if self.command_groups[commandgroup].commands[commandno] == data: # both the command_group and the command already # exit - replace. # maybe ask the user if the want to replace this... command_found = 1 self.command_groups[commandgroup].commands[commandno] = data # stop hunting through the command groups. # "commandgroup" variable should be the key for the right # command group to modify break if command_group_found and not command_found: # append the new command to the command group's command list self.command_groups[commandgroup].commands.append(data) if not command_group_found: # This command group does not exist yet. create a new # command group and add it to the command_groups dictionary commandgroup_instance = commandgroup_class(data.group,data) self.command_groups[data.group] = commandgroup_instance #print 'after' #self.dump_command_groups() self.write_commands() self.draw() def add_replace_host(self,data): host_found = 0 host_group_found = 0 for hostgroup in self.host_groups.keys(): if data.group == hostgroup: host_group_found = 1 for hostno in range(len(self.host_groups[hostgroup].hosts)): if self.host_groups[hostgroup].hosts[hostno] == data: # both the host_group and the host already # exit - replace. # maybe ask the user if the want to replace this... host_found = 1 self.host_groups[hostgroup].hosts[hostno] = data # stop hunting through the host groups. # "hostgroup" variable should be the key for the right # host group to modify break if host_group_found and not host_found: # append the new host to the host group's host list self.host_groups[hostgroup].hosts.append(data) if not host_group_found: # This host group does not exist yet. create a new # host group and add it to the host_groups dictionary hostgroup_instance = hostgroup_class(data.group,[data]) self.host_groups[data.group] = hostgroup_instance #print 'after' #self.dump_host_groups() self.write_hosts() #read_hosts() self.draw() def add_host_callback(self, widget, data=None): data = hostpath_class('','','') sys.stdout.flush() if self.misc_config('Add host ', data, ['path', 'shortname', 'group'], 0): self.dump_host_groups() self.add_replace_host(data) # host_found = 0 # host_group_found = 0 # for hostgroup in self.host_groups.keys(): # if data.group == hostgroup: # host_group_found = 1 # for hostno in range(len(self.host_groups[hostgroup].hosts)): # if self.host_groups[hostgroup].hosts[hostno] == data: # # both the host_group and the host already # # exit - replace. # # maybe ask the user if the want to replace this... # host_found = 1 # self.host_groups[hostgroup].hosts[hostno] = data # # stop hunting through the host groups. # # "hostgroup" variable should be the key for the right # # host group to modify # break # if host_group_found and not host_found: # # append the new host to the host group's host list # self.host_groups[hostgroup].hosts.append(data) # if not host_group_found: # # This host group does not exist yet. create a new # # host group and add it to the host_groups dictionary # hostgroup_instance = hostgroup_class(data.group,data) # self.host_groups[data.group] = hostgroup_instance # #print 'after' # #self.dump_host_groups() # self.write_hosts() # self.draw() def dump_host_groups(self): for hostgroup in self.host_groups.keys(): print 'hostgroup',hostgroup for host in self.host_groups[hostgroup].hosts: print '\thost'+str(host) def dump_command_groups(self): for commandgroup in self.command_groups.keys(): print 'commandgroup',commandgroup for command in self.command_groups[commandgroup].commands: print '\tcommand'+str(command) def command_reconfig_callback(self, widget, data=None): if self.misc_config('Modify command '+data.shortname, data, ['command', 'shortname', 'group', 'rununder'], 1, self.delete_command): self.write_commands() self.draw() # elif result == gtk.RESPONSE_CLOSE: # do_user_just_closed_the_window() # dia.destroy() def host_reconfig_callback(self, widget, data=None): prior_data = copy.deepcopy(data) if self.misc_config('Modify host '+data.shortname, data, ['path', 'shortname', 'group'], 1, self.delete_host): if prior_data.group != data.group: self.delete_host(prior_data) self.add_replace_host(data) self.write_hosts() self.draw() # elif result == gtk.RESPONSE_CLOSE: # do_user_just_closed_the_window() # dia.destroy() def host_callback(self, widget, data=None): if widget.get_active(): self.active_hosts[data.__repr__()] = data if self.deferred_button.get_active(): # wait for the user to click "execute" before running # anything pass else: # check if something should be run right away self.check_run() else: if self.active_hosts.has_key(data.__repr__()): del self.active_hosts[data.__repr__()] def command_callback(self, widget, data=None): if widget.get_active(): self.active_commands[data.__repr__()] = data if self.deferred_button.get_active(): # wait for the user to click "execute" before running # anything pass else: # check if something should be run right away self.check_run() else: if self.active_commands.has_key(data.__repr__()): del self.active_commands[data.__repr__()] def check_run(self): if len(self.active_hosts) >= 1 and len(self.active_commands) >= 1: self.do_run() def do_run(self): #self.active_hosts.sort() #self.active_commands.sort() ah = self.active_hosts.keys() if len(ah) < 1: self.warn('Sorry, you need to select at least one host') return 0 ah.sort() ac = self.active_commands.keys() if len(ac) < 1: self.warn('Sorry, you need to select at least one command') return 0 ac.sort() for repetition in range(self.multiple_spinner.get_value_as_int()): for host in ah: for command in ac: print 'Will execute command',command,'on host',host # -t: allocate a tty even if a command is given # -4: use ipv4 only. Fedora Core 3's sshd sometimes only # sets up X11 forwarding for ipv6 without this. Note: # this did -not- help! # -c blowfish: faster encryption, ala Bruce Schneier cmd_to_write = 'deep-ssh "-t -c blowfish" "%s" "%s"' % (self.active_hosts[host].path,self.active_commands[command].command) fn = tempfn() file = open(fn,'w') file.write('#!/bin/sh\n') #file.write('# The file should exist in the filesystem, and continue to be visible\n') #file.write('# to the bourne shell, even if it no longer has a name...\n') # but it doesn't sometimes :) file.write('%s\n' % cmd_to_write) file.write('(sleep 600 && rm $0) &\n') file.close() os.chmod(fn,0700) if self.active_commands[command].rununder == '': # this is not a GUI command, so we run it in some form of # user-specified local terminal emulator or similar cmd = fn else: cmd = self.active_commands[command].rununder+" "+fn cmd = cmd + ' &' # maybe use daemon? Or just setsid? print 'cmd is',cmd pid = os.fork() if pid == 0: # this setsid seems to help pursuade ssh to ask for # passwords and such graphically, instead of via a tty. # For this application at least, that's a good thing. os.setsid() #for fd in range(10): # os.close(fd) os.system(cmd) time.sleep(self.secs_between_spinner.get_value_as_int()) sys.exit(0) else: retval = os.waitpid(pid,0) print 'retval is',retval if self.stayopen_button.get_active(): # if the user wants sshgui to stick around, just clear the # previous buttons selected #print 'stayopen button is active' self.deselect_all() self.multiple_spinner.set_value(1.0) self.secs_between_spinner.set_value(5.0) else: # otherwise we exit #self.window.destroy() #gtk.main_quit() # give the command(s) we just started time to fire up... # the user has a chance to change their mind :) time.sleep(8) if self.stayopen_button.get_active(): self.deselect_all() self.multiple_spinner.set_value(1.0) self.secs_between_spinner.set_value(5.0) else: # exit! sys.exit(0) def toggle_hostgroup(self, widget, hostgroup): #print type(hostgroup) #print hostgroup #print hostgroup.__repr__() for host in hostgroup.hosts: host.select_button.set_active(not host.select_button.get_active()) def toggle_commandgroup(self, widget, commandgroup): #print type(commandgroup) #print commandgroup #print commandgroup.__repr__() for command in commandgroup.commands: command.select_button.set_active(not command.select_button.get_active()) def deselect_all(self): for host in self.active_hosts.keys(): self.active_hosts[host].select_button.set_active(False) for command in self.active_commands.keys(): self.active_commands[command].select_button.set_active(False) # this is really optional here, but removing it was masking an error I # wanted to track down, so I put it back #self.draw() def deferred_callback(self, widget, data=None): #print 'deferred callback' #self.draw() if self.deferred_button.get_active(): self.execute_button.set_sensitive(True) else: self.execute_button.set_sensitive(False) # This callback quits the program def delete_event(self, widget, event, data=None): # might put in a "Are you sure you want to exit?", but probably # not for this app :) gtk.main_quit() return False # def set_global_color(self,button): # label = button.get_child() # #button.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("red")) # label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("darkred")) # this had to be modified to handle the spin button def set_global_color(self,button): if hasattr(button,'get_child'): button.get_child().modify_fg(gtk.STATE_NORMAL, \ gtk.gdk.color_parse("darkred")) def set_host_color(self,button): label = button.get_child() label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("darkgreen")) def set_command_color(self,button): label = button.get_child() label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("darkblue")) def draw(self): # create the vbox's, which will hold the hbox of configuration # buttons, and a table. Delete it if it already existed, so we can start over with a clean slate #print 'starting draw()' if hasattr(self,'Vbox'): for child in self.window.get_children(): self.window.remove(child) for child in self.Vbox.get_children(): self.Vbox.remove(child) for child in self.host_vbox.get_children(): self.host_vbox.remove(child) for child in self.command_vbox.get_children(): self.command_vbox.remove(child) for child in self.scrolled_host_window.get_children(): self.scrolled_host_window.remove(child) for child in self.scrolled_command_window.get_children(): self.scrolled_command_window.remove(child) for child in self.hbox.get_children(): self.hbox.remove(child) self.Vbox = gtk.VBox() self.scrolled_host_window = gtk.ScrolledWindow() self.scrolled_host_window.set_border_width(10) #self.scrolled_host_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) self.scrolled_host_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrolled_command_window = gtk.ScrolledWindow() self.scrolled_command_window.set_border_width(10) self.scrolled_command_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.host_vbox = gtk.VBox() self.command_vbox = gtk.VBox() self.scrolled_host_window.add_with_viewport(self.host_vbox) self.scrolled_command_window.add_with_viewport(self.command_vbox) # if not hasattr(self,'hbox'): # create the hbox, which will hold our configuration buttons self.hbox = gtk.HBox() #cmap = self.hbox.get_colormap() #color=cmap.alloc_color('red') #self.hbox.modify_fg(gtk.STATE_NORMAL, color) # the stay open button self.stayopen_button = gtk.ToggleButton('Stay open') self.set_global_color(self.stayopen_button) self.tooltips.set_tip(self.stayopen_button,"Keeps sshgui open after running a host x command pairing") self.stayopen_button.connect("clicked", self.do_nothing) self.stayopen_button.show() self.hbox.pack_start(self.stayopen_button) # the deferred button. This really needs a tooltip if not hasattr(self,'deferred_button'): self.deferred_button = gtk.ToggleButton('Deferred') self.set_global_color(self.deferred_button) self.tooltips.set_tip(self.deferred_button,"Run n commands on m hosts") self.deferred_button.connect("clicked", self.deferred_callback) self.deferred_button.show() self.hbox.pack_start(self.deferred_button) # the execute button if not hasattr(self,'execute_button'): self.execute_button = gtk.Button('Execute') self.set_global_color(self.execute_button) self.tooltips.set_tip(self.execute_button,"Executes n commands on m hosts, after clicking the \"Deferred\" button") self.execute_button.connect("clicked", self.execute_callback) self.execute_button.show() if self.deferred_button.get_active(): self.execute_button.set_sensitive(True) else: self.execute_button.set_sensitive(False) self.hbox.pack_start(self.execute_button) # the multiple spinner if not hasattr(self,'multiple_spinner'): #self.multiple_adj = gtk.Adjustment(1.0, 1.0, 99.0, 1.0, 2.0, 0.0) self.multiple_adj = gtk.Adjustment(1, 1, 9, 1, 1, 0) self.multiple_spinner = gtk.SpinButton(self.multiple_adj, 0, 0) self.multiple_spinner.set_digits(0) self.multiple_spinner.set_wrap(True) # this errors #self.multiple_spinner.set_shadow_type(gtk.SHADOW_OUT) self.hbox.pack_start(self.multiple_spinner,False) self.set_global_color(self.multiple_spinner) self.tooltips.set_tip(self.multiple_spinner,"Run the same host x command pairings multiple times") self.multiple_spinner.show() else: self.hbox.pack_start(self.multiple_spinner,False) # the secs_between spinner if not hasattr(self,'secs_between_spinner'): self.secs_between_adj = gtk.Adjustment(5, 0, 30, 1, 1, 0) self.secs_between_spinner = gtk.SpinButton(self.secs_between_adj, 0, 0) self.secs_between_spinner.set_digits(0) self.secs_between_spinner.set_wrap(True) # this errors #self.secs_between_spinner.set_shadow_type(gtk.SHADOW_OUT) self.hbox.pack_start(self.secs_between_spinner,False) self.set_global_color(self.secs_between_spinner) self.tooltips.set_tip(self.secs_between_spinner,"Seconds between command invocations") self.secs_between_spinner.show() else: self.hbox.pack_start(self.secs_between_spinner,False) # the deselect all button button = gtk.Button('Deselect all') self.set_global_color(button) self.tooltips.set_tip(button,"Deselects all hosts and commands") button.connect("clicked", self.deselect_callback) button.show() self.hbox.pack_start(button) # the add host button button = gtk.Button('Add host') self.set_global_color(button) button.connect("clicked", self.add_host_callback) button.show() self.hbox.pack_start(button) # the add command button button = gtk.Button('Add command') self.set_global_color(button) button.connect("clicked", self.add_command_callback) button.show() self.hbox.pack_start(button) # the Exit button button = gtk.Button('Exit') self.set_global_color(button) button.connect("clicked", lambda w: gtk.main_quit()) button.show() self.hbox.pack_start(button) # the "About sshgui" button button = gtk.Button('About sshgui') self.set_global_color(button) button.connect("clicked", self.about_callback) button.show() self.hbox.pack_start(button) # the "Help" button button = gtk.Button('Help') self.set_global_color(button) button.connect("clicked", self.help_callback) button.show() self.hbox.pack_start(button) self.Vbox.pack_start(self.hbox,False,False) self.hbox.show() if 1 == 1: pass # Create a table which will hold a list of hosts hostgroups = self.host_groups.keys() hostgroups.sort() cols = 4 for hostgroup in hostgroups: numhostgroups = len(hostgroups) # number of rows: round up rows = (numhostgroups + cols - 1) / cols if hasattr(self.host_groups[hostgroup],'HSeparator'): for widget in self.host_groups[hostgroup].table.get_children(): #print widget self.host_groups[hostgroup].table.remove(widget) del self.host_groups[hostgroup].table self.host_vbox.pack_start(self.host_groups[hostgroup].HSeparator,False,False) self.host_groups[hostgroup].HSeparator.show() self.host_vbox.pack_start(self.host_groups[hostgroup].hostgroup_button,False,False) self.host_groups[hostgroup].hostgroup_button.show() self.host_groups[hostgroup].table = gtk.Table(cols, rows, False) else: self.host_groups[hostgroup].HSeparator = gtk.HSeparator() self.host_groups[hostgroup].HSeparator.show() #self.host_groups[hostgroup].HSeparator = gtk.HSeparator() #self.host_groups[hostgroup].HSeparator.show() self.host_vbox.pack_start(self.host_groups[hostgroup].HSeparator) self.host_groups[hostgroup].hostgroup_button = gtk.Button(hostgroup) self.set_host_color(self.host_groups[hostgroup].hostgroup_button) self.host_groups[hostgroup].hostgroup_button.connect('clicked',self.toggle_hostgroup,self.host_groups[hostgroup]) self.tooltips.set_tip(self.host_groups[hostgroup].hostgroup_button,"Toggle selection of this host group") self.host_groups[hostgroup].hostgroup_button.show() self.host_vbox.pack_start(self.host_groups[hostgroup].hostgroup_button) self.host_groups[hostgroup].table = gtk.Table(cols, rows, False) hosts = self.host_groups[hostgroup].hosts #print 'before sort',hosts hosts.sort() #print 'after sort',hosts numhosts = len(hosts) for hostno in range(numhosts): row = hostno / cols col = hostno - row*cols # self.hostgroups is a dict of items of type hostgroup_class, indexed by hostgroup name #print self.host_groups if hasattr(self.host_groups[hostgroup].hosts[hostno],'select_button'): # reattach the already created button - possibly in a different place on the screen self.host_groups[hostgroup].table.attach(self.host_groups[hostgroup].hosts[hostno].hbox,col,col+1,row,row+1) else: self.host_groups[hostgroup].hosts[hostno].hbox = gtk.HBox() self.host_groups[hostgroup].hosts[hostno].config_button = gtk.Button('^') self.set_host_color(self.host_groups[hostgroup].hosts[hostno].config_button) #self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].config_button, # "Modify " + self.host_groups[hostgroup].hosts[hostno].shortname) #self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].select_button, # "Select " + self.host_groups[hostgroup].hosts[hostno].path) self.host_groups[hostgroup].hosts[hostno].select_button = \ gtk.ToggleButton() self.host_groups[hostgroup].hosts[hostno].hbox.pack_start( \ self.host_groups[hostgroup].hosts[hostno].config_button, False,True) self.host_groups[hostgroup].hosts[hostno].hbox.pack_start(self.host_groups[hostgroup].hosts[hostno].select_button, \ False,True) self.host_groups[hostgroup].hosts[hostno].select_button.connect("clicked",self.host_callback, \ self.host_groups[hostgroup].hosts[hostno]) self.host_groups[hostgroup].hosts[hostno].config_button.connect("clicked",self.host_reconfig_callback, \ self.host_groups[hostgroup].hosts[hostno]) self.host_groups[hostgroup].hosts[hostno].select_button.show() self.host_groups[hostgroup].hosts[hostno].config_button.show() self.host_groups[hostgroup].hosts[hostno].hbox.show() self.host_groups[hostgroup].table.attach(self.host_groups[hostgroup].hosts[hostno].hbox, \ col,col+1,row,row+1) # change the button's label whether the button preexisted or # not self.host_groups[hostgroup].hosts[hostno].select_button.set_label(self.host_groups[hostgroup].hosts[hostno].shortname) # change the label's color, whether the button preexisted or # not self.set_host_color(self.host_groups[hostgroup].hosts[hostno].select_button) # redefine the relevant tooltips regardless as well... self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].config_button, "Modify " + self.host_groups[hostgroup].hosts[hostno].shortname) self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].select_button, "Select " + self.host_groups[hostgroup].hosts[hostno].path) self.host_groups[hostgroup].table.show() self.host_vbox.pack_start(self.host_groups[hostgroup].table) self.host_vbox.show() self.Vbox.pack_start(self.scrolled_host_window) # Create a table which will hold a list of commands commandgroups = self.command_groups.keys() commandgroups.sort() #cols = 4 for commandgroup in commandgroups: numcommandgroups = len(commandgroups) # number of rows: round up rows = (numcommandgroups + cols - 1) / cols if hasattr(self.command_groups[commandgroup],'HSeparator'): for widget in self.command_groups[commandgroup].table.get_children(): self.command_groups[commandgroup].table.remove(widget) del self.command_groups[commandgroup].table self.command_vbox.pack_start(self.command_groups[commandgroup].HSeparator,False,False) self.command_groups[commandgroup].HSeparator.show() self.command_vbox.pack_start(self.command_groups[commandgroup].commandgroup_button,False,False) self.command_groups[commandgroup].commandgroup_button.show() self.command_groups[commandgroup].table = gtk.Table(cols, rows, False) else: self.command_groups[commandgroup].HSeparator = gtk.HSeparator() self.command_groups[commandgroup].HSeparator.show() self.command_vbox.pack_start(self.command_groups[commandgroup].HSeparator) self.command_groups[commandgroup].commandgroup_button = gtk.Button(commandgroup) self.set_command_color(self.command_groups[commandgroup].commandgroup_button) self.command_groups[commandgroup].commandgroup_button.connect('clicked',self.toggle_commandgroup,self.command_groups[commandgroup]) self.tooltips.set_tip(self.command_groups[commandgroup].commandgroup_button,"Toggle selection of this command group") self.command_groups[commandgroup].commandgroup_button.show() self.command_vbox.pack_start(self.command_groups[commandgroup].commandgroup_button) self.command_groups[commandgroup].table = gtk.Table(cols, rows, False) commands = self.command_groups[commandgroup].commands commands.sort() numcommands = len(commands) for commandno in range(numcommands): row = commandno / cols col = commandno - row*cols # self.commandgroups is a dict of items of type commandgroup_class, indexed by commandgroup name #print self.command_groups if hasattr(self.command_groups[commandgroup].commands[commandno],'select_button'): # reattach the already created button - possibly in a different place on the screen self.command_groups[commandgroup].table.attach(self.command_groups[commandgroup].commands[commandno].hbox,col,col+1,row,row+1) else: self.command_groups[commandgroup].commands[commandno].hbox = gtk.HBox() self.command_groups[commandgroup].commands[commandno].config_button = gtk.Button('^') self.set_command_color(self.command_groups[commandgroup].commands[commandno].config_button) self.tooltips.set_tip(self.command_groups[commandgroup].commands[commandno].config_button, \ "Modify " + self.command_groups[commandgroup].commands[commandno].shortname) self.command_groups[commandgroup].commands[commandno].select_button = \ gtk.ToggleButton(self.command_groups[commandgroup].commands[commandno].shortname) self.set_command_color(self.command_groups[commandgroup].commands[commandno].select_button) self.tooltips.set_tip(self.command_groups[commandgroup].commands[commandno].select_button, \ "Select " + self.command_groups[commandgroup].commands[commandno].command) self.command_groups[commandgroup].commands[commandno].hbox.pack_start( \ self.command_groups[commandgroup].commands[commandno].config_button, False,True) self.command_groups[commandgroup].commands[commandno].hbox.pack_start(self.command_groups[commandgroup].commands[commandno].select_button, \ False,True) self.command_groups[commandgroup].commands[commandno].select_button.connect("clicked",self.command_callback, \ self.command_groups[commandgroup].commands[commandno]) self.command_groups[commandgroup].commands[commandno].config_button.connect("clicked",self.command_reconfig_callback, \ self.command_groups[commandgroup].commands[commandno]) self.command_groups[commandgroup].commands[commandno].select_button.show() self.command_groups[commandgroup].commands[commandno].config_button.show() self.command_groups[commandgroup].commands[commandno].hbox.show() self.command_groups[commandgroup].table.attach(self.command_groups[commandgroup].commands[commandno].hbox, \ col,col+1,row,row+1) self.command_groups[commandgroup].table.show() self.command_vbox.pack_start(self.command_groups[commandgroup].table) self.command_vbox.show() self.Vbox.pack_start(self.scrolled_command_window) self.scrolled_host_window.show() self.scrolled_command_window.show() self.Vbox.show() self.window.add(self.Vbox) self.window.show() #print 'end of draw' def __init__(self): # Create a new window self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) # Set the window title self.window.set_title("sshgui") # Set a handler for delete_event that immediately # exits GTK. self.window.connect("delete_event", self.delete_event) # Sets the border width of the window. self.window.set_border_width(20) self.command_groups = read_commands() self.host_groups = read_hosts() self.window.set_size_request(1280, 950) self.active_hosts = {} self.active_commands = {} self.tooltips = gtk.Tooltips() self.version = 0.5 self.draw() def read_hosts(): dict = read_it('~/.sshgui/hosts', hostpath_class, [ 'path', 'shortname', 'group' ]) keys = dict.keys() host_groups = {} for key in keys: #print key,dict[key] host_groups[key] = hostgroup_class(key,dict[key]) return host_groups def read_commands(): dict = read_it('~/.sshgui/commands', command_class, [ 'command', 'shortname', 'group', 'rununder' ]) keys = dict.keys() command_groups = {} for key in keys: command_groups[key] = commandgroup_class(key,dict[key]) return command_groups def main(): gtk.main() return 0 if __name__ == "__main__": GUI() main()