#!/usr/bin/env python # This is the "boil the ocean" version :) # before this, there was sshgui and sshgrid # Filesystem structure: # ~/.hcm # /hosts # /host_groupname1 # /hostname1 # /hostname2 # ... # /hostnamen # /host_groupname2 # /hostname1 # /hostname2 # ... # /hostnamen # ... # /host_groupnamen # /hostname1 # /hostname2 # ... # /hostnamen # /commands # /command_groupname1 # /commandname1 # /commandname2 # ... # /commandnamen # /command_groupname2 # /commandname1 # /commandname2 # ... # /commandnamen # ... # /command_groupnamen # /commandname1 # /commandname2 # ... # /commandnamen # # So for example, the second node in Teoco would be: # ~/.hcm/hosts/Teoco/teoco!node2 # # As another example, the xterm command in the X11 command group would look like: # ~/.hcm/commands/X11/xterm # # So inhaling all the command groups amounts to cd'ing to the ~/.hcm/commands directory, and inhaling # all the directories as command groups # # Then inhaling a command group amounts to cd'ing to ~/.hcm/commands/X11 (for example), and inhaling # all the files as commands # # So our group_of_groups_class needs ~/.hcm/commands in an instance variable # # Our command_group instance (which is a genericgroup_class instance, really) needs ~/.hcm/commands/command_groupname # in an instance variable # # Our command_class needs the file ~/.hcm/commands/command_groupname/commandname in an instance variable #import traceback # There are two kinds of deletions in the program: # 1) In which we delete a single item. This means: # A) Delete the item itself and remove it from the GUI # B) Delete the item from the containing genericgroup_class instance (IE element of host_group or element of command_group). Nothing needs # to change in the GUI for this part # 2) In which we delete a entire hostgroup. This means: # A) Delete all items in the hostgroup or command group, and remove them from the GUI # B) Delete the hostgroup or commandgroup instance itself (IE element of host_group or command_group) and remove its name from the GUI too # C) Delete the remnant of the hostgroup or commandgroup from the containing group_of_groups # # In both cases, it's probably best to initiate the relevant deletions from the most general container, descend down to the most specific, # and delete backward in generality as we traverse back # # The question arises, is there much commonality between the two? Can #2 wrap #1 plus a bit? # # first of all: # group_of_groups_class instances contain multiple genericgroup_class instances # genericgroup_class instances contain multiple item_class instances # # #1 looks like: # In genericgroup_class instance: # delete single item_class instance # delete item from genericgroup_class instance # rebuild genericgroup_class portion of GUI - or the whole thing? # # #2 looks like: # In group_of_groups instance: # descend into single relevant genericgroup_class instance # for each item in genericgroup_class instance: # delete single item # delete item from genericgroup_class instance # don't rebuild the GUI just yet - it'll change more times than really necessary if one does # return back to the group_of_groups_instance # delete the genericgroup_class instance # rebuild the relevant part of the GUI - or the whole thing? # Wow, this whole frame for a function thing... # there should be frames for: # 1) Selecting things to run and running them # A) Just a list of hostgroups, hostnames, command groups and command names. To keep things simple, we probably only # need a toggle function, unless perhaps we get a little fancier with the host groups and command groups # B) This is the point of this program, so is the highest priority :) # C) Inherits from generic_operation_gui_builder # 2) Modifying an item's details (excluding containing groups) # A) Just a list of hostnames and command names; no groups needed. Click an expander, get a bunch of questions # B) This is relatively difficult to do with a text editor, mostly becaus of the lengths that need to be preserved. # However, it would not be terribly difficult to come up with a format that Could be modified with a text editor... Hmm... # C) Inherits from generic_operation_gui_builder # 3) Adding an item to a genericgroup (EG, a host to a preexisting hostgroup) # B) Adding a host to a hostgroup # D) Adding a command to a command group # E) This is relatively difficult to do with standard *ix shell commands, so is a relatively high priority. However, it's not a really # high priority, because this program's commandline interface allows this operation # F) Does -not- inherit from generic_operation_gui_builder # 4) Deleting items from a genericgroup # A) Just a list of hostgroups, hostnames, command groups and command names # B) Inherits from generic_operation_gui_builder # C) Nonessential - this is easy to do with rm # D) Inherits from generic_operation_gui_builder # 5) Renaming items # A) for each of hosts and commands # I) A list of items # II) A "rename selected button" # B) Nonessential - this is easy to do with mv # C) Inherits from generic_operation_gui_builder # 6) Merging host groups # A) Just a list of host groups and command groups, but each group listed is a menu of other groups one might merge with # B) Nonessential - this is easy to do with mv # C) Inherits from generic_operation_gui_builder # 7) Splitting host groups or command groups # A) A list of items within groups, with the items togglable and the groups toggling all items within them # B) A text entry box for the new hostgroup name # C) Nonessential - this is easy to do with mkdir and mv # D) Inherits from generic_operation_gui_builder # 8) Adding a hostgroup or commandgroup (genericgroup) # A) Adding a hostgroup # B) Adding a commandgroup # C) Nonessential - this is easy to do with mkdir # D) Does -not- inherit from generic_operation_gui_builder # 9) Deleting genericgroups # A) Just a list of hostgroups and command groups # B) Does -not- inherit from generic_operation_gui_builder # C) Nonessential - this is easy to do with rm -rf and/or rm in combination with rmdir # D) Does -not- inherit from generic_operation_gui_builder # # In each of these scenarios, we can employ a Frame under a Notebook. I've got it doing a notebook now, but in retrospect, maybe a # bunch of expanders would've been nice... # # In each of these scenarios that involve a list of items within hostgroups and commandgroups, we would benefit from a class # that knows: # 1) how to put some operation-specific stuff across the top of the frame # 2) how to iterate over all items, and how to set up a button for each item - with the former being in a base clase, and the latter # being in distinct subclasses for each task import os import re import sys import time import fcntl import string import shutil sys.path.append('/usr/local/lib') sys.path.append(os.path.expanduser('~/lib')) import bashquote program_name = 'hcm' program_version=0.4 def gui_up(): if 'gtk' in sys.modules.keys(): return True else: return False def ensure_directory(directory): try: os.mkdir(directory) except (OSError, 17): pass def my_chdir(directory): ensure_directory(directory) os.chdir(directory) def ignore(filename): if filename.startswith('.'): # skip dot files return 1 if filename.endswith('~'): # skip emacs backup files return 1 return 0 base_directory=os.path.join(os.path.expanduser('~'), '.%s' % program_name) ensure_directory(base_directory) # This callback quits the program def delete_event(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 save_changes_and_exit(*args): print 'Saving changes...' host_group.exhale() command_group.exhale() print 'Terminating normally...' if gui_up(): return delete_event('ignored1', 'ignored2') else: sys.exit(0) def strip_eol(line): if line.endswith('\n'): return line[:-1] else: return line def list_operation(left_list, right_list, function_for_left_list_only, function_for_right_list_only, function_for_in_both_lists): temp_left_list = left_list[:] temp_right_list = right_list[:] temp_left_list.sort() temp_right_list.sort() left_list_index = 0 right_list_index = 0 left_list_length = len(temp_left_list) right_list_length = len(temp_right_list) while left_list_index < left_list_length or right_list_index < right_list_length: if left_list_index >= left_list_length: # left_list is used up - add something from right_list function_for_right_list_only(temp_right_list[right_list_index]) right_list_index += 1 elif right_list_index >= right_list_length: # right_list is used up - add something from left_list function_for_left_list_only(temp_left_list[left_list_index]) left_list_index += 1 # neither list is used up elif temp_left_list[left_list_index] < temp_right_list[right_list_index]: # left_list's value is lower - add it function_for_left_list_only(temp_left_list[left_list_index]) left_list_index += 1 elif temp_left_list[left_list_index] > temp_right_list[right_list_index]: # right_list's value is lower - add it function_for_right_list_only(temp_right_list[right_list_index]) right_list_index += 1 else: # left_list's value is the same as right_list's, add them together function_for_in_both_lists(temp_left_list[left_list_index]) left_list_index += 1 right_list_index += 1 def temp_filename(): return '/tmp/%s-%d-%d' % (program_name, os.getpid(), int(time.time() * 10000)) def hex_nybble(nybble): return '0123456789abcdef'[nybble] def hex_byte(byte): return '%s%s' % (hex_nybble(byte/16), hex_nybble(byte & 0xf)) def random_string(): file = open('/dev/urandom', 'r') bytes = file.read(16) file.close() result = '' for byte in bytes: result += hex_byte(ord(byte)) return result def logging_stamp(): if 1 == 1: return '`logging-stamp`' else: # this is a nice thought, but then it's the same file for all tabs in # mrxvt, which isn't so nice ^_^ return '%s-R%s' % (time.strftime('%Y-%m-%d-%a'), random_string()) def run_individual_match(host, command): if host.dict['path'].startswith('!'): # Here we're making a deep-ssh bang path have a new interpretation - if the bang path starts with a bang, # then run it locally, not in deep-ssh. print 'Locally spawning command %s on host %s' % (command.dict['name'], host.dict['name']) path = host.dict['path'][1:] print 'path is', path bq = bashquote.bashquote() bq.add(path) command_to_system = '%s %s' % (command.dict['command'], str(bq)) else: print 'Spawning command %s on host %s' % (command.dict['name'], host.dict['name']) # arcfour is apparently a little faster than blowfish, at least according to PSC's web page about ssh performance patches. # Null encryption is probably faster than that... But that only would encrypt authentication, not data transmission. bq = bashquote.bashquote() bq.add(command.dict['command']) command_to_write = "deep-ssh '%s' %s --ssh-options '-t -c arcfour'" % (host.dict['path'], str(bq)) bq2 = bashquote.bashquote() bq2.add(command_to_write) logdir = os.path.expanduser('~/.hcm/logs') ensure_directory(logdir) os.chdir(logdir) command_to_write = "pypty -d -f -c %s ~/.hcm/logs/%s" % (str(bq2), logging_stamp()) filename = temp_filename() file = open(filename,'w') file.write('#!/bin/sh\n\n') file.write('if [ "$1" = "" ]\n') file.write('then') file.write('\ttitle="."\n') file.write('else') file.write('\ttitle="$1"\n') file.write('fi\n') file.write('which xname > /dev/null 2>&1 && xname "$title"\n\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') # except that it doesn't sometimes ^_^ file.write('%s\n' % command_to_write) file.write('\n') # We don't actually want this rm, because a multi-tabbed terminal emulator that can run more than one invocation of a single command at # different times will get confused later when trying to spawn a new command from this file. It'd be better to have a cron job # that cleans out anything over a couple of months old or something - but then sometimes my sessions last more than a couple of months... #file.write('(sleep 600 && rm $0) &\n') file.close() os.chmod(filename,0700) if command.dict['rununder'] == '': # this is a GUI command, so we do not need to run it in some form of user-specified local terminal # emulator or similar command_to_system = filename else: #print 'host.group_name is',host.group_name,'in run_individual_match' rununder_with_replacements = command.dict['rununder']. \ replace('#g', host.group_name). \ replace('#n', host.dict['name']). \ replace('##', '#') #command_to_system = '%s %s "%s / %s"' % \ # (rununder_with_replacements, filename, host.group_name, host.dict['name']) command_to_system = '%s %s' % \ (rununder_with_replacements, filename) command_to_system += ' &' # maybe use daemon? Or just setsid? I heard once that fork()'ing twice migh help, too print 'command_to_system is',command_to_system 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() os.system(command_to_system) time.sleep(gui.builders['Run'].seconds_between_commands_spinner.get_value_as_int()) sys.exit(0) else: retval = os.waitpid(pid,0) print 'retval is',retval def run_all_host_command_matches(): selected_hosts = host_group.get_selected('Run') selected_hosts.sort() selected_commands = command_group.get_selected('Run') selected_commands.sort() for repetition in range(gui.builders['Run'].command_repetition_spinner.get_value_as_int()): for host in selected_hosts: for command in selected_commands: run_individual_match(host, command) if gui.builders['Run'].stayopen_button.get_active(): # if the stayopen button is active, then we don't automatically exit return else: # the stayopen button wasn't active, so we do exit. We may need to sleep a bit actually, but for now, we just exit sys.exit(0) def run_if_appropriate_conditions(): host_count = host_group.count_selected('Run') command_count = command_group.count_selected('Run') if gui.builders['Run'].deferred_button.get_active(): # if we're in deferred mode, then we never run until the user clicks the execute button return else: # if we're not in deferred mode, then we run as soon as both the selected host count and the selected command count are above 0 if host_count > 0 and command_count > 0: run_all_host_command_matches() #gss_count=0 #sss=0 #toggle_selected_reentry_count = 0 set_selection_state_reentry_count = 0 toggle_selected_reentry_count = 0 class my_button: # this may help with inverting buttons... Since get_active/set_active seem very confused def __init__(self, button, state): self.button = button self.state = state self.count = 0 # this too_many/up/down stuff is basically a sloppy mutex def __too_many(self): if self.count >= 1: self.count = 0 return 1 else: return 0 def __up(self): if self.count >= 0: self.count += 1 else: self.count = 1 def __down(self): if self.count >= 1: self.count -= 1 else: self.count = 0 def set_true(self): if self.__too_many(): return self.__up() self.button.set_active(True) self.__down() self.state = True def set_false(self): if self.__too_many(): return self.__up() self.button.set_active(False) self.__down() self.state = False def invert(self): if self.__too_many(): return self.__up() self.state = not self.state self.button.set_active(self.state) self.__down() def get(self): return self.state def connect(self, *args): apply(self.button.connect, args) def get_children(self): if hasattr(self.button, 'get_children'): return self.button.get_children() else: return [] def show(self): self.button.show() # instances of this class will hold an individual host or an individual command class item_class: def __init__(self, filename, group_name, *args): #print 'group_name is',group_name,'in item_class constructor' self.filename = filename self.group_name = group_name length = len(args) if length != 0 and length != len(self.my_fields): raise ("Bad number of arguments to item_class constructor: Wanted 2 or %d, got %d" % (len(self.my_fields)+2, len(args)+2)) elif length == 0: if type(self.filename) == type(''): # assume this is a filename self.inhale(self.filename) else: raise ("Bad number of arguments to item_class constructor: Wanted 2 or %d, got %d" % (len(self.my_fields)+2, len(args)+2)) elif length == len(self.my_fields): self.dict={} for fieldno in xrange(len(args)): self.dict[self.my_fields[fieldno]] = args[fieldno] else: sys.stderr.write(sys.argv[0], 'Eh? This should not happen\n' % sys.argv[0]) sys.exit(1) self.selected=False def noop(self, widget=None): pass return true def get_selection_state(self, button_type): return self.buttons[button_type].get() def mark_selected(self, button_type, individual): self.buttons[button_type].set_true() run_if_appropriate_conditions() def toggle_selected(self, button_type, widget=None, individual=True): self.buttons[button_type].invert() run_if_appropriate_conditions() def mark_deselected(self, button_type, individual): self.buttons[button_type].set_false() run_if_appropriate_conditions() def __cmp__(self, other): # only the name matters in comparisons if self.dict['name'] < other.dict['name']: return -1 elif self.dict['name'] > other.dict['name']: return 1 else: return 0 def inhale(self, filename): self.filename = filename file = open(filename, 'r') number_of_fields = string.atoi(file.readline()) fields_from_file = [] for fieldno in xrange(number_of_fields): fields_from_file.append(strip_eol(file.readline())) if self.my_fields != fields_from_file: error('Field list mismatch in %s: wanted %s, got %s\n' % \ (self.filename, str(fields_from_file), str(self.my_fields))) self.dict = {} for field in self.my_fields: try: length = string.atoi(file.readline()) except ValueError: sys.stderr.write('%s: Not an integer in %s\n' % \ (sys.argv[0], self.filename)) sys.exit(1) self.dict[field] = strip_eol(file.read(length)) dummy = file.readline() if dummy != '\n': sys.stderr.write('field anchor incorrect in %s\n' % self.filename) file.close() def exhale(self): file = open(self.filename, 'w') # write the number of fields, followed by a newline file.write(str(len(self.my_fields))) file.write('\n') # write the field names, separated by newlines for field in self.my_fields: file.write(field) file.write('\n') # write each field for field in self.my_fields: # write the field value length followed by a newline length = len(self.dict[field]) file.write(str(length)) file.write('\n') # write the field value followed by a newline file.write(self.dict[field]) file.write('\n') file.close() def __str__(self): result = '' for field in self.my_fields: result += ' ' + self.dict[field] return result def setup_gui_objects(self): self.buttons = {} for button_type in ['Run']: self.buttons[button_type] = my_button(gtk.ToggleButton(self.dict['name']), self.selected) color_traversal(self.buttons[button_type], self.color) self.buttons[button_type].connect('toggled', lambda widget: self.toggle_selected(button_type, widget)) # a little strangely named relative to this class in isolation since it will always be 0 or 1, but it makes sense # when considered in the context of some of the other classes in this program since they can have greater counts def count_selected(self, button_type): if self.get_selection_state(button_type): return 1 else: return 0 def delete(self): pass # an instance of this class will hold an individual host class host_class(item_class): my_fields = ['name', 'path', 'sshport'] color = 'darkred' # an instance of this class will hold an individual command class command_class(item_class): my_fields = ['name', 'command', 'rununder'] color = 'darkgreen' # this will hold individual command groups and individual host groups - IOW, a group of hosts, or a group of commands class genericgroup_class: def __init__(self, name, item_class_parameter, base_directory): self.name = name self.collection = {} #self.text_color = text_color self.base_directory = base_directory self.directory = os.path.join(self.base_directory, self.name) self.item_class = item_class_parameter # will be either host_class or command_class def mark_selected(self, button_type, data=None): keys = self.collection.keys() for key in keys: self.collection[key].mark_selected(button_type, False) run_if_appropriate_conditions() def toggle_selected(self, button_type, data=None): keys = self.collection.keys() for key in keys: self.collection[key].toggle_selected(button_type, False) run_if_appropriate_conditions() def mark_deselected(self, button_type, data=None): keys = self.collection.keys() for key in keys: self.collection[key].mark_deselected(button_type, False) # this should never actually be true anyway #run_if_appropriate_conditions() def add_or_replace(self, name, item): self.collection[name] = item def delete(self, item_name): if self.collection.has_key(item_name): del self.collection[item_name] else: sys.stderr.write('%s: %s %s already does not exist in %s group %s\n' % \ (sys.argv[0], self.group_type, item_name, self.group_type, self.name)) if not ignore_already: sys.exit(1) def keys(self): return self.collection.keys() def __getitem__(self, key): return self.collection[key] def __delitem__(self, key): del self.collection[key] def __str__(self): result = '%s' % self.name keys = self.collection.keys() keys.sort() # we're not including self.name in our output, because it's also in the item for key in keys: result += ' %s' % str(self.collection[key]) return result def inhale(self): my_chdir(self.directory) self.collection = {} for itemname in os.listdir('.'): if ignore(itemname): continue self.collection[itemname] = self.item_class(os.path.join(self.directory, itemname), self.name) def exhale(self): in_memory_list = self.collection.keys() my_chdir(self.directory) on_disk_list = filter(lambda filename: not ignore(filename), os.listdir('.')) list_operation(in_memory_list, on_disk_list, \ lambda itemname: self.collection[itemname].exhale(), \ os.unlink, \ lambda itemname: self.collection[itemname].exhale(), \ ) def hasitem(self, key): return key in self.collection def rename(self, widget=None): pass def delete(self, item_name): self.collection[item_name].delete() def count_selected(self, button_type): keys = self.collection.keys() selected = 0 for key in keys: selected += self.collection[key].count_selected(button_type) return selected def get_selected(self, button_type): keys = self.collection.keys() selected = [] for key in keys: if self.collection[key].get_selection_state(button_type): selected.append(self.collection[key]) return selected class host_group_class(genericgroup_class): # all host_groups will be dark red in the GUI color = 'darkred' group_type='host' #group_of_groups=host_group class command_group_class(genericgroup_class): # all command_groups will be dark green in the GUI color = 'darkgreen' group_type='command' #group_of_groups=command_group # there will be two of these - one for all host_groups, and one for all command_groups class group_of_groups_class: def __init__(self, name, group_class_parameter, item_class_parameter, directory): self.name = name self.collection = {} self.group_class = group_class_parameter self.item_class = item_class_parameter self.directory = os.path.join(os.path.expanduser(directory), self.name) def keys(self): return self.collection.keys() def has_key(self, key): return self.collection.has_key(key) def __getitem__(self, key): return self.collection[key] def __setitem__(self, key, value): self.collection[key] = value # this one just reaks of abstraction violation, but I think that's probably essential def __delitem__(self, key): if self.collection.has_key(key): group = self.collection[key] for item in group.keys(): group[item].delete() del group[item] del self.collection[key] else: sys.stderr.write('%s: %s group %s already did not exist\n' % (sys.argv[0], self.name, key)) if not ignore_already: sys.exit(1) def inhale(self): my_chdir(self.directory) for filename in os.listdir('.'): if ignore(filename): continue self.collection[filename] = self.group_class(filename, self.item_class, self.directory) self.collection[filename].inhale() def exhale(self): in_memory_list = self.collection.keys() my_chdir(self.directory) on_disk_list = filter(lambda filename: not ignore(filename), os.listdir('.')) list_operation(in_memory_list, on_disk_list, \ lambda groupname: self.collection[groupname].exhale(), \ lambda directory_name: shutil.rmtree(os.path.join(self.directory, directory_name)), \ lambda groupname: self.collection[groupname].exhale(), \ ) def for_gui(self): widget = gtk.VBox() temp = gtk.Label('groups of %s' % self.name) temp.show() widget.add(temp) keys = self.collection.keys() keys.sort() for key in keys: temp = self.collection[key].for_gui() temp.show() widget.pack_start(temp, expand=False, fill=False) return widget def mark_deselected(self, button_type): keys = self.collection.keys() for key in keys: self.collection[key].mark_deselected(False) def count_selected(self, button_type): keys = self.collection.keys() selected = 0 for key in keys: selected += self.collection[key].count_selected(button_type) return selected def get_selected(self, button_type): keys = self.collection.keys() selected = [] for key in keys: selected += self.collection[key].get_selected(button_type) return selected def delete(self, widget=None, group_name=''): del self.collection[group_ame] return True def color_traversal(gui_stuff, color): # I'm thinking the stack requirements won't actually be that hefty, at least as long as we're talking about a hierarchy and not a proper graph ^_^ if hasattr(gui_stuff, 'get_children'): for child in gui_stuff.get_children(): color_traversal(child, color) if hasattr(gui_stuff, 'modify_fg'): gui_stuff.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) class generic_operation_gui_builder_class: across_color = 'darkblue' # group_list remains constant until the instance is destroyed, but the GUI stuff is recreated each time gimme_gui is called # This works because aggregate types are pass by reference in python def __init__(self, group_of_groups_list): # in practice, this will always be host groups and command groups (host_group and command_group, but they're dictionaries of hostgroups and # commandgroups) self.group_of_groups_list = group_of_groups_list def for_gui(self): # the whole schmear gets packed into this self.vbox = gtk.VBox() # just one of these self.vbox.pack_start(self.across_the_top(), expand=False, fill=False) # might put a separator here someday - maybe a double separator even for group_of_groups in self.group_of_groups_list: # one of these for each group_of_groups - IE, one for hosts, one for commands, and don't sort them, then loop on genericgroups result = gtk.Label('%s' % group_of_groups.name) result.show() self.vbox.pack_start(result, expand=False, fill=False) # we do sort genericgroups - they're things like "X11" or "Sears Singlecore" genericgroup_names = group_of_groups.keys() genericgroup_names.sort() scrolled_window = gtk.ScrolledWindow() scrolled_window.set_border_width(10) scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scrolled_window.show() box = gtk.VBox() scrolled_window.add_with_viewport(box) box.show() self.vbox.pack_start(scrolled_window, expand=True, fill=True) for genericgroup_name in genericgroup_names: genericgroup = group_of_groups[genericgroup_name] # maybe a separator here someday # one of these for each genericgroup, followed by the stuff for each item result = self.gui_for_genericgroup_proper(genericgroup) color_traversal(result, genericgroup.color) result.show() box.pack_start(result, expand=False, fill=False) # we do sort on item names - they're things like "xterm" or "node11" item_names = genericgroup.keys() item_names.sort() cols = 10 # round up rows = (len(item_names) + cols - 1) / cols table = gtk.Table(cols, rows, False) table.show() item_number=0 #print 'len(item_names)',len(item_names), 'rows',rows, 'cols',cols for item_name in item_names: item = genericgroup[item_name] # one of these for each item. For now, we arrange them strictly vertically, but later we'll # most likely want a table of them, and it's possible the dimensions of that table should be # controlled somehow by the subclass result = self.gui_for_item(item, item_name) #print result color_traversal(result, genericgroup.color) #print result result.show() col = item_number / rows row = item_number % rows #print 'row',row, 'col',col table.attach(result.button, col, col+1, row, row+1) #box.pack_start(result.button, expand=False, fill=False) item_number += 1 box.pack_start(table, expand=False, fill=False) # might put a separator here someday - at the end of the genericgroup self.vbox.show() return self.vbox class select_gui_builder_class(generic_operation_gui_builder_class): shortname = 'Run' longname = 'Select host * command matchups and run them' ########################################### # Start of across_the_top related methods # ########################################### def stayopen_callback(self, widget, data=None): # intentionally doing nothing - we just interrogate the state of the button itself when needed pass return True def create_stayopen_button(self): # the stay open button if not hasattr(self, 'stayopen_button'): self.stayopen_button = gtk.ToggleButton('Stay open') set_button_color(self.stayopen_button, self.across_color) self.stayopen_button.set_tooltip_text("Keeps %s open after running a host x command matchup" % program_name) self.stayopen_button.connect("toggled", self.stayopen_callback) self.stayopen_button.show() return self.stayopen_button def deferred_callback(self, widget, data=None): if self.deferred_button.get_active(): self.execute_button.set_sensitive(True) else: self.execute_button.set_sensitive(False) run_if_appropriate_conditions() return True def create_deferred_button(self): # the deferred button if not hasattr(self,'deferred_button'): self.deferred_button = gtk.ToggleButton('Deferred') set_button_color(self.deferred_button, self.across_color) self.deferred_button.set_tooltip_text("Run n commands on m hosts instead of the usual 1 x n or n x 1") self.deferred_button.connect("toggled", self.deferred_callback) self.deferred_button.show() return self.deferred_button def execute_callback(self, widget, data=None): # unconditionally - no need to run_if_appropriate_conditions() ^_^ In fact, we -shouldn't- use run_if_appropriate_conditions() run_all_host_command_matches() return True def create_execute_button(self): # the execute button if not hasattr(self,'execute_button'): self.execute_button = gtk.Button('Execute') set_button_color(self.execute_button, self.across_color) self.execute_button.set_tooltip_text("Execute n commands on m hosts, after clicking the \"Deferred\" button") self.execute_button.connect("clicked", self.execute_callback) # if the deferred button exists, pay attention to it - otherwise, just assume this execute button should be insensitive if hasattr(self, 'deferred_button'): if self.deferred_button.get_active(): self.execute_button.set_sensitive(True) else: self.execute_button.set_sensitive(False) else: self.execute_button.set_sensitive(False) return self.execute_button def create_command_repetition_spinner(self): # the multiple spinner if not hasattr(self,'command_repetition_spinner'): self.multiple_adj = gtk.Adjustment(1, 1, 9, 1, 1, 0) self.command_repetition_spinner = gtk.SpinButton(self.multiple_adj, 0, 0) self.command_repetition_spinner.set_digits(0) self.command_repetition_spinner.set_wrap(True) set_button_color(self.command_repetition_spinner, self.across_color) self.command_repetition_spinner.set_tooltip_text("Run the same host x command pairings multiple times") return self.command_repetition_spinner def create_seconds_between_commands_spinner(self): # the secs_between spinner if not hasattr(self,'seconds_between_commands_spinner'): self.secs_between_adj = gtk.Adjustment(2, 0, 30, 1, 1, 0) self.seconds_between_commands_spinner = gtk.SpinButton(self.secs_between_adj, 0, 0) self.seconds_between_commands_spinner.set_digits(0) self.seconds_between_commands_spinner.set_wrap(True) set_button_color(self.seconds_between_commands_spinner, self.across_color) self.seconds_between_commands_spinner.set_tooltip_text("Seconds between command invocations") return self.seconds_between_commands_spinner def for_across_the_top(self): if not hasattr(self, 'hbox'): self.hbox = gtk.HBox() # these widgets will be added to the "top row" from left to right with consistent coloring (at least for the buttons, that is - # the spinners come up looking unchanged) for widget in [ \ self.create_execute_button(), \ self.create_deferred_button(), \ self.create_stayopen_button(), \ # self.create_deselect_all_button(), \ self.create_seconds_between_commands_spinner(), \ self.create_command_repetition_spinner(), \ ]: #set_button_color(widget, self.color) #self.hbox.pack_end(widget, spacing=10) self.hbox.pack_start(widget, expand=False, fill=False) widget.show() #color_traversal(self.hbox, 'pink') self.hbox.show() return self.hbox def across_the_top(self): self.widget = self.for_across_the_top() return self.widget ############################################################################################### # End of across_the_top related methods, start of gui_for_genericgroup_proper related methods # ############################################################################################### def gui_for_genericgroup_proper(self, genericgroup): vbox = gtk.VBox() hbox = gtk.HBox() vbox.pack_start(hbox, expand=False, fill=False) result = gtk.Label(genericgroup.name) result.show() hbox.pack_start(result, expand=False, fill=False) result = gtk.Button('Select') result.connect('clicked', lambda widget: genericgroup.mark_selected('Run', widget)) result.show() hbox.pack_start(result, expand=False, fill=False) result = gtk.Button('Toggle') result.connect('clicked', lambda widget: genericgroup.toggle_selected('Run', widget)) result.show() hbox.pack_start(result, expand=False, fill=False) result = gtk.Button('Deselect') result.connect('clicked', lambda widget: genericgroup.mark_deselected('Run', widget)) result.show() hbox.pack_start(result, expand=False, fill=False) vbox.show() hbox.show() return vbox ############################################################################################# # End of gui_for_genericgroup_proper related methods, start of gui_for_item related methods # ############################################################################################# def gui_for_item(self, item, item_name): item.setup_gui_objects() return item.buttons['Run'] class generic_stub_operation_gui_builder_class: def __init__(self, group_of_groups_list): self.group_of_groups_list = group_of_groups_list def for_gui(self): self.vbox = gtk.VBox() self.vbox.show() label = gtk.Label('placeholder for %s' % self.longname) label.show() self.vbox.pack_start(label, expand=False, fill=False) label = gtk.Label('') label.show() self.vbox.pack_start(label, expand=False, fill=False) label=gtk.Label('Sorry, you cannot perform this operation via the GUI yet') label.show() self.vbox.pack_start(label, expand=False, fill=False) label = gtk.Label('') label.show() self.vbox.pack_start(label, expand=False, fill=False) if hasattr(self,'workaround'): label=gtk.Label('However you may be able to do the following at the command line:') label.show() self.vbox.pack_start(label, expand=False, fill=False) for line in string.splitfields(self.workaround, '\n'): label = gtk.Label(line) label.show() self.vbox.pack_start(label, expand=False, fill=False) return self.vbox class modify_details_class(generic_stub_operation_gui_builder_class): shortname = 'modify' longname = 'modify the details of a host or command' workaround = 'There is not really a good workaround for this.\n'+ \ 'You are probably best off using a text editor and correcting lengths,\n'+ \ 'or waiting for this to be implemented.' class add_item_class(generic_stub_operation_gui_builder_class): shortname = 'add item' longname = 'add a host to a preexisting hostgroup or command to a preexisting commandgroup' workaround = 'Use %s --add-host or %s --add-command.' % (program_name, program_name) class delete_item_class(generic_stub_operation_gui_builder_class): shortname = 'delete item' longname = 'delete a host from a hostgroup or a command from a commandgroup' workaround = 'To delete a host:\n'+ \ 'cd ~/.hcm/hosts\n'+ \ 'cd hostgroupname\n'+ \ 'rm hostname' class rename_class(generic_stub_operation_gui_builder_class): shortname = 'rename' longname = 'rename an item or group' workaround = 'To rename a commandgroup:\n'+ \ 'stop %s whether by exiting the GUI or with the kill command' % program_name + \ 'cd ~/.hcm/commands\n'+ \ 'mv oldcommandgroupname newcommandgroupname\n'+ \ '\n'+ \ 'To rename a host\n'+ \ 'stop %s whether by exiting the GUI or with the kill command' % program_name + \ 'cd ~/.hcm/hosts\n'+ \ 'cd hostgroupname\n'+ \ 'mv oldhostname newhostname' class merge_class(generic_stub_operation_gui_builder_class): shortname = 'merge' longname = 'merge items from one group to another' workaround = 'To merge hostgroups X and Y:\n'+ \ 'stop %s whether by exiting the GUI or with the kill command' % program_name + \ 'cd ~/.hcm/hosts\n'+ \ 'mv X/* Y/.\n'+ \ 'rmdir Y' class split_class(generic_stub_operation_gui_builder_class): shortname = 'split' longname = 'split a group in two' workaround = 'To split hosts starting with a through m in hostgroups X into hostgroup Y:\n'+ \ 'stop %s whether by exiting the GUI or with the kill command' % program_name + \ 'cd ~/.hcm/hosts\n'+ \ 'mkdir Y\n'+ \ 'mv X/[a-m]* Y/.' class add_group_class(generic_stub_operation_gui_builder_class): shortname = 'add group' longname = 'add a new hostgroup or commandgroup' workaround = 'To create a new hostgroup named H:\n'+ \ 'stop %s whether by exiting the GUI or with the kill command' % program_name + \ 'cd ~/.hcm/hosts\n'+ \ 'mkdir H' class delete_group_class(generic_stub_operation_gui_builder_class): shortname = 'delete group' longname = 'delete a hostgroup or commandgroup' workaround = 'To delete a hostgroup named H:\n'+ \ 'stop %s whether by exiting the GUI or with the kill command' % program_name + \ 'cd ~/.hcm/hosts\n'+ \ 'rm -rf H' # This function (and the myriad calls of it) probably should just go away altogether at some point, but I haven't really # tested color_traversal yet ^_^ def set_button_color(button, color): #if hasattr(button, 'get_child'): # button.get_child().modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) color_traversal(button, color) # this should probably use gui_up to either give a CLI error or GUI error def error_via_gui(self, message): dia = gtk.Dialog('%s error!' % program_name, gui.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) # this should probably use gui_up to either give a CLI error or GUI error def warn_via_gui(self, message): dia = gtk.Dialog('%s warning' % program_name, gui.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) class always_top_row_class: color = 'black' # def deselect_callback(self, widget, data=None): # host_group.mark_deselected() # command_group.mark_deselected() # return True # # def create_deselect_all_button(self): # # the deselect all button # if not hasattr(self, "deselect_all"): # self.deselect_all_button = gtk.Button('Deselect all') # set_button_color(self.deselect_all_button, self.color) # tooltips.set_tip(self.deselect_all_button, "Deselects all hosts and commands") # self.deselect_all_button.connect("clicked", self.deselect_callback) # return self.deselect_all_button # def add_group_callback(self): # sys.stderr.write('Not implemented\n') # # def create_add_host_group_button(self): # # the add host group button # if not hasattr(self, 'add_host_group_button'): # self.add_host_group_button = gtk.Button('Add host group') # set_button_color(self.add_host_group_button, self.color) # self.add_host_group_button.connect("clicked", self.add_group_callback) # return self.add_host_group_button # def create_add_command_group_button(self): # # the add command group button # if not hasattr(self, 'add_command_group_button'): # self.add_command_group_button = gtk.Button('Add command group') # set_button_color(self.add_command_group_button, self.color) # self.add_command_group_button.connect("clicked", self.add_group_callback) # return self.add_command_group_button def create_exit_button(self): # the Exit button # someday this should save changes! if not hasattr(self, 'exit_button'): self.exit_button = gtk.Button('Exit') set_button_color(self.exit_button, self.color) self.exit_button.connect("clicked", lambda ignored_argument: save_changes_and_exit(ignored_argument)) self.exit_button.set_tooltip_text("Save changes and terminate the program") return self.exit_button def create_quit_button(self): # the Quit button if not hasattr(self, 'quit_button'): self.quit_button = gtk.Button('Quit') set_button_color(self.quit_button, self.color) self.quit_button.connect("clicked", lambda w: sys.exit(0)) self.quit_button.set_tooltip_text("Terminate the program without saving changes") return self.quit_button def about_callback(self, widget, data=None): dia = gtk.Dialog('About %s' % program_name, gui.window.get_toplevel(), \ gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123)) dia.set_size_request(500, 300) for line in [ \ '%s is by Dan Stromberg' % program_name, \ 'You are looking at version '+str(program_version), \ 'http://stromberg.dnsalias.org/~strombrg/%s/' % program_name, \ 'Please see the FAQ at http://stromberg.dnsalias.org/~strombrg/%s/FAQ.html' % program_name, \ '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') return True def create_about_button(self): # the About button if not hasattr(self, 'about_button'): self.about_button = gtk.Button('About %s' % program_name) set_button_color(self.about_button, self.color) self.about_button.connect("clicked", self.about_callback) return self.about_button def help_callback(self, widget, data=None): dia = gtk.Dialog('%s help' % program_name, gui.window.get_toplevel(), \ gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123)) dia.set_size_request(500, 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') return True def create_help_button(self): # the "Help" button if not hasattr(self, 'help_button'): self.help_button = gtk.Button('Help') self.help_button.connect("clicked", self.help_callback) return self.help_button def for_gui(self): if not hasattr(self, 'hbox'): self.hbox = gtk.HBox() # these widgets will be added to the "top row" from left to right with consistent coloring (at least for the buttons, that is - # the spinners come up looking unchanged) for widget in [ \ self.create_help_button(), \ self.create_about_button(), \ self.create_exit_button(), \ self.create_quit_button(), \ ]: set_button_color(widget, self.color) #self.hbox.pack_end(widget, spacing=10) self.hbox.pack_start(widget, expand=False, fill=False) widget.show() self.hbox.show() return self.hbox def __init__(self): self.widget = self.for_gui() def show(self): self.widget.show() class GUI: def __init__(self): # Create a new window self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) # Set the window title self.window.set_title(program_name) # Set a handler for delete_event that immediately exits GTK. self.window.connect("delete_event", delete_event) # Sets the border width of the window. self.window.set_border_width(20) # set the dimensions of the enclosing window self.window.set_size_request(1270, 950) self.vbox = gtk.VBox() self.vbox.show() self.always_top_row = always_top_row_class() self.always_top_row.show() self.vbox.pack_start(self.always_top_row.widget, expand=False, fill=False) notebook = gtk.Notebook() notebook.show() self.builders = {} for builder_class in [ select_gui_builder_class, \ modify_details_class, \ add_item_class, \ delete_item_class, \ rename_class, \ merge_class, \ split_class, \ add_group_class, \ delete_group_class, ]: builder = builder_class([host_group, command_group]) self.builders[builder.shortname] = builder frame = gtk.Frame(builder.longname) frame.add(builder.for_gui()) frame.show() label = gtk.Label(builder.shortname) label.show() notebook.append_page(frame, label) self.vbox.pack_start(notebook, expand=True, fill=True) self.window.add(self.vbox) self.window.show() def usage(retval): sys.stderr.write('Usage: %s\n' % program_name) sys.stderr.write('\t--add-host-group host_groupname\n') sys.stderr.write('\t--add-command-group command_groupname\n') sys.stderr.write('\t--add-host host_groupname hostname sshpath sshport\n') sys.stderr.write('\t--add-command command_groupname commandname command rununder\n') sys.stderr.write('\t--delete-host host_groupname hostname\n') sys.stderr.write('\t--delete-command command_groupname commandname\n') sys.stderr.write('\t--delete-host-group host_groupname\n') sys.stderr.write('\t--delete-command-group command_groupname\n') sys.stderr.write('\t--dump-host-groups\n') sys.stderr.write('\t--dump-command-groups\n') sys.stderr.write('\t--gui\n') sys.stderr.write('\t--ignore-already\n') sys.stderr.write('\t--already\n') sys.stderr.write('\t--help\n') sys.exit(retval) def dump_group(group): group_keys = group.keys() group_keys.sort() for gk in group_keys: print gk, str(group[gk]) def add_group(group_type, group_class, item_class, group, group_name): if group.has_key(group_name): sys.stderr.write('%s: %s group %s already exists\n' % (sys.argv[0], group_type, group_name)) if not ignore_already: sys.exit(1) else: group_directory = os.path.join(base_directory, '%ss' % group_type) ensure_directory(group_directory) #group[group_name] = group_class('%ss' % group_type, item_class, group_directory) group[group_name] = group_class(group_name, item_class, group_directory) def add_item(item_type, item_class, group, group_to_add_to, item_to_add, *item_specific_parameters): if group.has_key(group_to_add_to): if group[group_to_add_to].hasitem(item_to_add): sys.stderr.write('%s: %s %s already exists in %s group %s\n' % (sys.argv[0], item_type, item_to_add, item_type, group_to_add_to)) if not ignore_already: sys.exit(1) group_directory = os.path.join(base_directory, '%ss' % item_type) ensure_directory(group_directory) constructor_parameters = [] constructor_parameters.append(os.path.join(os.path.join(group_directory, group_to_add_to), item_to_add)) constructor_parameters.append(group_to_add_to) constructor_parameters.append(item_to_add) for i in list(item_specific_parameters): constructor_parameters.append(i) group[group_to_add_to].add_or_replace(item_to_add, apply(item_class, constructor_parameters)) else: sys.stderr.write('%s: %s group %s does not exist. Consider adding it with --add-%s-group\n' % \ (program_name, item_type, group_to_add_to, item_type)) sys.exit(1) def delete_item(group_type, group, group_name_to_remove_from, item_name): if group.has_key(group_name_to_remove_from): group[group_name_to_remove_from].delete(item_name) else: sys.stderr.write('%s: %s group %s already nonexistent\n' % (sys.argv[0], group_type, group_name_to_remove_from)) if not ignore_already: sys.exit(1) def delete_group(group_type, group, group_name_to_remove): if group.has_key(group_name_to_remove): del group[group_name_to_remove] else: sys.stderr.write('%s: %s group %s already nonexistent\n' % (sys.argv[0], group_type, group_name_to_remove_from)) if not ignore_already: sys.exit(1) def drop_argv(n): for i in xrange(n): del sys.argv[1] if __name__ == "__main__": # everything needs to read, so we always read (some things only need host groups or command groups though) host_group = group_of_groups_class('hosts', host_group_class, host_class, '~/.%s' % program_name) host_group.inhale() command_group = group_of_groups_class('commands', command_group_class, command_class, '~/.%s' % program_name) command_group.inhale() write_at_end=0 ignore_already=0 add_group_regex = re.compile('--add-([^-]*)-group') add_item_regex = re.compile('--add-([^-]*)') delete_item_regex = re.compile('--delete-([^-]*)') delete_group_regex = re.compile('--delete-([^-]*)-group') add_group_dict={} add_group_dict['host'] = [host_group_class, host_class, host_group] add_group_dict['command'] = [command_group_class, command_class, command_group] add_item_dict={} add_item_dict['host'] = [host_class, host_group] add_item_dict['command'] = [command_class, command_group] delete_item_dict={} delete_item_dict['host'] = [host_group] delete_item_dict['command'] = [command_group] delete_group_dict = delete_item_dict #sys.stderr.write('foo\n') while sys.argv[1:]: if sys.argv[1] in ['--add-host-group', '--add-command-group']: group_type_match = add_group_regex.match(sys.argv[1]) group_type = group_type_match.group(1) group_name_to_add = sys.argv[2] args = [group_type] + add_group_dict[group_type] + [group_name_to_add] apply(add_group, args) drop_argv(1) write_at_end = 1 elif sys.argv[1] in ['--add-host', '--add-command']: item_type_match = add_item_regex.match(sys.argv[1]) item_type = item_type_match.group(1) group_to_add_to = sys.argv[2] name_to_add = sys.argv[3] # in the case of an -add-host, argv[4] and argv[5] are the sshpath (bang path) and sshport # in the case of an -add-command, argv[4] and argv[5] are the full command and the rununder command args = [item_type] + add_item_dict[item_type] + [group_to_add_to, name_to_add, sys.argv[4], sys.argv[5]] apply(add_item, args) drop_argv(4) write_at_end = 1 elif sys.argv[1] in ['--delete-host', '--delete-command']: delete_item_match = delete_item_regex.match(sys.argv[1]) item_type = delete_item_match.group(1) group_name_to_delete_from = sys.argv[2] item_to_delete = sys.argv[3] args = [item_type] + delete_item_dict[item_type] + [group_name_to_delete_from, item_to_delete] apply(delete_item, args) drop_argv(2) write_at_end = 1 elif sys.argv[1] in ['--delete-host-group', '--delete-command-group']: delete_group_match = delete_group_regex.match(sys.argv[1]) group_type = delete_group_match.group(1) group_name_to_delete = sys.argv[2] args = [group_type] + delete_group_dict[group_type] + [group_name_to_delete] apply(delete_group, args) drop_argv(1) write_at_end = 1 elif sys.argv[1] == '--dump-host-groups': dump_group(host_group) elif sys.argv[1] == '--dump-command-groups': dump_group(command_group) elif sys.argv[1] == '--gui': import pygtk pygtk.require('2.0') import gtk gui = GUI() gtk.main() elif sys.argv[1] == '--ignore-already': ignore_already=1 elif sys.argv[1] == '--already': ignore_already=0 elif sys.argv[1] in ['--help', '-h']: usage(0) else: sys.stderr.write('%s: Illegal argument: %s\n' % (sys.argv[0], sys.argv[1])) usage(1) drop_argv(1) #sys.stderr.write('bar\n') if write_at_end: save_changes_and_exit() else: sys.exit(0)