import repository from arizona
[raven.git] / apps / gui2 / storkslicemanager.py
1 #!/usr/bin/env python
2
3 #           [option, long option,                    variable,            action,        data,     default,                            metavar,    description]
4 """arizonaconfig
5    options=[   
6             ["",   "--managerconf",        "managerconf",        "store",    "string",    "~/.storkmanager.conf",               "FILE",      "use a different config file (~/.storkmanager.conf is the default)"],
7             ["",   "--repositoryhost",     "repositoryhost",     "store",    "string",    "stork-repository.cs.arizona.edu",    "FILE",      "use a different repository (among other repository settings) (stork-repository.cs.arizona.edu is the default)"], 
8             ["",   "--curlpath",           "curlpath",           "store",    "string",    "/usr/bin/curl",                      "FILE",      "the path to the curl executable (/usr/bin/curl is the default)"]
9    ]
10    includes=[]        
11 """
12
13
14 from Tkinter import *
15 from tkMessageBox import *
16 from Tkinter import _setit
17 import tkFileDialog
18 import tkFont
19 import os
20 import shutil
21 import sys
22 import re
23 import webbrowser
24
25 import storkcurlfuncs as sc
26 import arizonaconfig
27 import arizonacrypt
28 import arizonageneral
29 import arizonareport
30 import planetlabAPI
31 import storkpackage
32 import storkpackagelist
33 import storkusername as storkusernamepackage
34 import storkutil
35
36
37
38 #the version of the gui, this number should be incremented
39 #whenever there is a change to this file, or a change to 
40 guiversion = "$Revision: 2.30 $"
41
42 checkedforupdate = False
43
44 repository = None
45
46 debug = False
47
48 groupframes = {} 
49 groups = []
50 nodes  = {} #a dict of groupname->array of nodes          
51 actions= {}
52 to_upload=[]
53
54 next_group_row = 0
55 switch_user = False
56
57 # whether the current state is in synch with the repository
58 synched = True
59
60 root = None
61
62 topoptions = None
63 username = None
64 password = None
65 #poor choice of name for storkusername variable as it conflicts with the module of the same name
66 storkusername = None
67 privatekey = None
68 publickey = None
69 slicekeys = None
70
71 # copies of these files that are are signed by the key the user selected
72 # these will be updated from the repository when a slice is selected
73 localconffile = None
74 localtpfile = None
75 localpackagesfile = None
76 localgroupsfile = None
77
78 # copies of the files that are the extracted versions of these files
79 unsignedconffile = None
80 unsignedtpfile = None
81 unsignedpackagesfile = None
82 unsignedgroupsfile = None
83
84 # Flags that will always be passed to storkutil to identify this user. 
85 # This gets set later, don't add extra flags to this here.
86 storkutiluserflags = None
87
88 # The directory the script is located in.
89 scriptpath = os.path.realpath(os.path.dirname(sys.argv[0]))
90
91 # Path to images used by the GUI.
92 imagepath = os.path.join(scriptpath, "images")
93
94 # filename of the gui config file
95 config_fn = os.path.expanduser("~/.storkmanager.conf")
96
97 # Path to where all non-temp files that are read and created.
98 # Note: not fully implemented yet. Leave empty to have it be ignored.
99 # The idea would be to make setting/changing the working directory
100 # available through the GUI, but this works for testing.
101 # Example:
102 #workingpath = "/home/myusername/somedirectory";
103 #workingpath = "/home/justin/school/research/stork/9-test"
104 workingpath = ""
105 commandstub = ""
106
107 # Change working directory so files are read from and created there.
108 # This would be done somewhere else once one can set this from the GUI.
109 if workingpath:
110     arizonareport.send_out(2, "Changing working directory to: "+workingpath)
111     os.chdir(workingpath)
112
113
114 # Change the commands for different OS
115 #HOME is for Linux. Command should be '../storkutil.py' For windows, 
116 #it is simply 'storkutil.py'
117 if 'HOME' in os.environ:
118     commandstub = '/'
119
120
121 def runCommand(command, stdin_string=None):
122     """Executes a shell command.
123     
124     command:      the shell command to execute
125     stdin_string: Optional string containing data to be sent to the command
126                   through stdin.
127                   
128     returns a tuple of (stdout_sl, stderr_sl) 
129     """
130     arizonareport.send_out(2, "About to run command: " + str(command))
131     (the_in, the_out, the_err) = os.popen3(command)
132     
133     if stdin_string != None:
134         the_in.write(stdin_string)
135     the_in.close()
136
137     out_sl = arizonageneral.stream_to_sl(the_out)
138     the_out.close()
139
140     err_sl = arizonageneral.stream_to_sl(the_err)
141     the_err.close()
142
143     arizonareport.send_out(4, "   Last command stdout: " + str(out_sl))
144     arizonareport.send_out(4, "   Last command stderr: " + str(err_sl))
145     
146     return out_sl, err_sl
147
148
149 class StorkGuiException(Exception):
150     """Just a quick way to stop using generic exceptions until something better
151        is setup for stork exceptions.
152     """
153     pass
154
155
156 def extractSignedFile(signedfile, extractedfile):
157     """Extracts the original file from an signed file.
158        
159        signedfile: the signed file
160        extractedfile: the file where the extracted contents should be saved
161     """
162     
163     global storkutiluserflags
164     
165     command = scriptpath+commandstub+'storkutil.py extract '+signedfile+' '+extractedfile
166     runCommand(command)
167
168
169 def signFile(file):
170     """Signs file with the user's key."""
171     
172     global storkutiluserflags
173     
174     command = scriptpath+commandstub+'storkutil.py '+storkutiluserflags
175     if privatekey.password != None:
176        command += " --privatekeypasslocation=stdin "
177     command += ' sign '+file
178     (out_sl, err_sl) = runCommand(command, privatekey.password)
179     
180     
181 def copyFile(src, dst):
182     """Copies src to dst, overwriting dst if it exists."""
183     
184     if os.path.exists(dst):
185         os.remove(dst)
186     shutil.copy(src, dst)
187
188
189 def removeFile(file):
190     """Removes file if it exists."""
191     
192     if file and os.path.exists(file):
193         os.remove(file)
194
195
196 def cleanUpFiles():
197     """Cleans up files the GUI created in the working directory."""
198
199     candidates = [localconffile,
200                   localtpfile,
201                   localpackagesfile,
202                   localgroupsfile,
203                   unsignedconffile,
204                   unsignedtpfile,
205                   unsignedpackagesfile,
206                   unsignedgroupsfile,
207                   "GetCookie"]
208     
209     for file in candidates:
210         removeFile(file)
211
212
213 def createLocalFileFromRepoFile(filetype, repofile):
214     """Given a filetype and a signed file from the repository that may be
215        signed by a different user's key, creates an unsigned version of the
216        file and a version signed by the current user's key.
217        
218        Returns a tuple of (path to unsigned file, path to signed file).
219     """
220     
221     global storkusername
222     global storkutiluserflags
223     
224     if filetype == 'packages':
225         ext = ".packages.pacman"
226     elif filetype == 'groups':
227         ext = ".groups.pacman"
228     elif filetype == 'tpfile':
229         ext = ".tpfile"
230     elif filetype == 'conf':
231         ext = ".stork.conf"
232     else:
233         raise StorkGuiException, "Unable to create local file from repo file: uknown filetype"
234         
235     # define the filesnames of the unsigned and signed files
236     unsignedfile = storkusername+ext
237     signedfile = storkusername+"."+getPubKeyHash()+ext
238     
239     # extract the repofile, which may be signed by someone else's key
240     extractSignedFile(repofile, unsignedfile)
241     
242     # copy the unsigned file to the location of the signed file
243     copyFile(unsignedfile, signedfile)
244     
245     # create the signed file that is signed by the current user's key
246     signFile(signedfile)
247     
248     return (unsignedfile, signedfile)
249     
250
251 def isPackage(path):
252    """Decides if path is the path to a valid package, checking only its
253       file extension for a match with ["tar", "tgz", "tbz","rpm"]."""
254       
255    # lets try not to tar or rpm in case they are on windows,
256    # so we will do 'dummy' checking by filename
257    base = os.path.basename(path)
258    for foo in ["tar", "tgz", "tbz","rpm"]:
259       if foo in base: return True
260
261    return False
262
263
264 def trustPackage(path):
265    """Calls storkutil.py addfile ..."""
266    
267    if not os.path.isfile(path):
268       return
269    command = scriptpath+'/storkutil.py '+storkutiluserflags
270    if privatekey.password != None:
271       command += " --privatekeypasslocation=stdin "
272    command += ' addfile '+path
273    (out_sl, err_sl) = runCommand(command, privatekey.password)
274    
275
276 def removeTrust(file):
277    """Calls storkutil.py removefile ..."""
278     
279    command = scriptpath+'/storkutil.py '+storkutiluserflags
280    if privatekey.password != None:
281       command += " --privatekeypasslocation=stdin "
282    command += ' removefile '+file
283    (out_sl, err_sl) = runCommand(command, privatekey.password)
284
285
286 def uploadPackage(path):
287    global repository
288    metahash = storkpackage.get_package_metadata_hash(path)
289    if not sc.url_exists("metadata/"+repository+"_packages_PlanetLab_V3_Testing/"+str(metahash) ):
290       # sense it does not already seem to be on the repository, upload it
291       sc.upload_file(username, password, path, "package")
292    else:
293       print "debug: "+path+" exists on server, not uploading"  
294
295
296 #def makeKey(user):
297 #   """Generates a keypair for the user using storkutil.py genpair.
298 #      This isn't used anymore since the change was made to have planetlab keys used
299 #      rather than dedicated stork keys.
300 #   """
301 #   
302 #   command = scriptpath+commandstub+'storkutil.py --dontask --username='+user+' genpair '+user
303 #   runCommand(command)
304 #   # If statusframe is defined, then the main window is open and we want to
305 #   # change the syched state. Otherwise, we just want to flag that when the
306 #   # main window opens it should not show as synched.
307 #   global statusframe
308 #   try:
309 #       statusframe
310 #   except NameError:
311 #       global synched
312 #       synched = False
313 #   else:
314 #       statusframe.set_in_synch(False)
315
316
317 def getPubKeyHash():
318     """Returns the hash of the GUI user's public key."""
319     global publickey
320     return publickey.hash
321   
322
323 def getGroups():
324    """Returns a list of group names that exist based upon the current groups file.
325    """
326    # parsing xml by hand not so good, using something like xml.dom.minidom would
327    # be much better
328     
329    # because of the way it's written not using proper xml processing, and I don't feel
330    # like fixing it right now, just going to stick with having it process the signed
331    # files
332    #global unsignedgroupsfile
333    #global unsignedpackagesfile
334    global localgroupsfile
335    global localpackagesfile
336    
337    grps = []
338    grps.append("All")
339    if not localgroupsfile or not os.path.isfile(localgroupsfile):
340       arizonareport.send_out(2, "No groups file found. No groups will be added.")
341    else:
342       groupFile = open(localgroupsfile)
343       for line in groupFile:
344            if line.find("GROUP NAME") != -1:
345                groupname = line[16:-6]
346                grps.append(groupname)
347    # get empty groups that have something set for them in the packages file
348    if localpackagesfile and os.path.isfile(localpackagesfile):
349        packagesFile = open(localpackagesfile)
350        regex = re.compile(r'CONFIG GROUP="(.+?)"')
351        for line in packagesFile:
352            matches = regex.search(line)
353            if matches and matches.group(1) not in grps:
354                grps.append(matches.group(1))
355    return grps
356
357
358 def getNodes(group):
359     """Returns a list of nodes in the group specified, according to the current
360        groups file.
361     """
362     # parsing xml by hand not so good, using something like xml.dom.minidom would
363     # be much better
364     
365     # because of the way it's written not using proper xml processing, and I don't feel
366     # like fixing it right now, just going to stick with having it process the signed
367     # file
368     #global unsignedgroupsfile
369     global localgroupsfile
370     arizonareport.send_out(4, "Getting nodes for group: " + str(group))
371   
372     nodeList = []
373     if not localgroupsfile or not os.path.isfile(localgroupsfile):
374         arizonareport.send_out(2, "Nothing added to nodelist because there is no groups file to read from.")
375         return nodeList
376     else:
377         groupFile = open(localgroupsfile)
378         
379     groupstarted = False
380     
381     for line in groupFile:
382         arizonareport.send_out(4, "Current line of groups file: " + str(line).strip())
383
384         # the group is already started and we hit another group, so we're done with the file
385         if groupstarted == True and line.find("GROUP") != -1:
386             break
387         
388         # check if this is the start of the group
389         if line.find("\""+group+"\"") != -1:
390             arizonareport.send_out(4, "Found group " + str(group) + " in groups file.")
391             groupstarted = True
392             continue
393         
394         # we haven't found the group yet and we didn't find it on this line, so the contents
395         # of this line don't matter
396         if groupstarted == False:
397             continue
398         
399         # at this 
400         if line.find("INCLUDE") != -1:
401             arizonareport.send_out(3, "About to append: "+ str(line[18:-7]) + " to " + str(group))
402             nodeList.append(line[18:-7])
403
404     return nodeList
405
406
407 def getSlices():
408    """Returns a list of slices that this user has access to, or for some odd reason
409       a list containing a single string that says 'Could not fetch slices'.
410    """
411    
412    if debug: return ["arizona_client1"]
413
414    slices = sc.getslices()
415    if slices == None:
416       return ["Could not fetch slices"]
417    else:
418       return slices
419
420
421 def getSliceKeys(slicename):
422     """Ensures that storkusername's key database is using the keys for 
423        slicename.
424     """
425     
426     storkusernamepackage.reset_key_database()
427     storkusernamepackage.get_planetlab_publickeys(slicename)
428
429
430 def getActions(group):
431    """Returns a list of ??? that has info on the actions (what to install, upgrade, etc.)
432       that the GUI should display, based upon the current configuration files.
433    """
434    
435    # parsing xml by hand not so good, using something like xml.dom.minidom would
436    # be much better
437    
438    # because of the way it's written not using proper xml processing, and I don't feel
439    # like fixing it right now, just going to stick with having it process the signed
440    # file
441    #global unsignedpackagesfile
442    global localpackagesfile
443    
444    #Provides tuples of the form (action, package) for the provided group name. actions will be the returned
445    #list of tuples, curAct is the current action that needs to be tuplified, preLen is used to parse the 
446    #package name from the line
447    actions = []
448    curAct = ""
449    if not localpackagesfile or not os.path.isfile(localpackagesfile):
450         return actions
451    else:
452         try:
453            actionsFile = open(localpackagesfile, "r")
454         except:
455            return actions
456                 
457         while(True): #Loop through each line of the packages file
458                 outerLine = actionsFile.readline()
459                 
460                 if group == "All":
461                         if outerLine.find("CONFIG>") != -1:
462                                 while(True):
463                                         innerLine = actionsFile.readline()
464                                         if innerLine.find("/CONFIG") == -1:
465                                                 if innerLine.find("PACKAGE") != -1:
466                                                         if innerLine.find("INSTALL") != -1:
467                                                                 curAct = "install"
468                                                                 preLen = 21
469                                                         elif innerLine.find("UPDATE") != -1:
470                                                                 curAct = "update"
471                                                                 preLen = 20
472                                                         elif innerLine.find("REMOVE") != -1:
473                                                                 curAct = "remove"
474                                                                 preLen = 20
475                                                         curPack = innerLine[preLen:-7]
476                                                         actions.append( (curPack, curAct) )
477                                         else:
478                                                 break #If we looped through the whole group, stop
479                                 break #If we find the group, stop looping through the file
480                         if outerLine == "":
481                                 break                                           
482
483                 else: #The case where the group IS NOT "All"
484                         if outerLine.find("CONFIG GROUP=\""+group+"\"") != -1:
485                                 while(True):
486                                         innerLine = actionsFile.readline()
487                                         if innerLine.find("/CONFIG") == -1:
488                                                 if innerLine.find("PACKAGE") != -1:
489                                                         if innerLine.find("INSTALL") != -1:
490                                                                 curAct = "install"
491                                                                 preLen = 21
492                                                         elif innerLine.find("UPDATE") != -1:
493                                                                 curAct = "update"
494                                                                 preLen = 20
495                                                         elif innerLine.find("REMOVE") != -1:
496                                                                 curAct = "remove"
497                                                                 preLen = 20
498                                                         curPack = innerLine[preLen:-7]
499                                                         actions.append( (curPack, curAct) )
500                                         else:
501                                                 break #If we looped through the whole group, stop
502                                 break #If we find the group, stop looping through the file
503                         if outerLine == "":
504                                 break
505         actionsFile.close()
506         return actions
507
508
509 def createFiles():
510     """
511         <Purpose>
512         Creates pacman files, tpfile, config file and list of packages for upload.
513
514         <Arguments>   
515         None
516
517         <Exceptions>
518         None
519
520         <Side Effects>
521         Deletes and rebuilds the aforementioned files
522
523         <Returns>
524         None
525
526         Note: This function is currently done by simply invoking storkutil.py with the appropriate command
527         line commands. This can be cleaned up by replacing the "if args=..." statements in storkutil with 
528         functions. If this is done, this section will need to be modified.
529     """
530     
531     global localconffile
532     global localtpfile
533     global localpackagesfile
534     global localgroupsfile
535     
536     global unsignedconffile
537     
538     global privatekey
539     global publickey
540    
541     if localgroupsfile and os.path.isfile(localgroupsfile):
542         os.remove(localgroupsfile)
543     for gp in nodes:
544         nodelist = ''
545         for nd in nodes[gp]:
546                 nodelist += nd+' '
547         if nodelist != '':
548             command = scriptpath+'/storkutil.py '
549             if privatekey.password != None:
550                command += " --privatekeypasslocation=stdin "
551             # the "--privatekeypasslocation=stdin" has to be at least before "pacgroups" (I think)
552             command += storkutiluserflags+' pacgroups include '+"'"+gp+"'"+' '+nodelist
553             (out_sl, err_sl) = runCommand(command, privatekey.password)
554
555     localgroupsfile = storkusername+"."+getPubKeyHash()+".groups.pacman"
556     
557     #Create pacpackages file
558     if localpackagesfile and os.path.isfile(localpackagesfile): 
559         os.remove(localpackagesfile)
560     for gp in actions:
561         for act in actions[gp]:
562             if(gp == 'All'):
563                 command = scriptpath+'/storkutil.py '+storkutiluserflags
564                 if privatekey.password != None:
565                    command += " --privatekeypasslocation=stdin "
566                 command += ' pacpackages all '+act[1]+' '+act[0]
567                 (out_sl, err_sl) = runCommand(command, privatekey.password)
568                 
569             else:
570                 command = scriptpath+'/storkutil.py '+storkutiluserflags
571                 if privatekey.password != None:
572                    command += " --privatekeypasslocation=stdin "
573                 command += ' pacpackages group '+"'"+gp+"'"+' '+act[1]+' '+act[0]
574                 (out_sl, err_sl) = runCommand(command, privatekey.password)
575                 
576         localpackagesfile = storkusername+"."+getPubKeyHash()+".packages.pacman"
577         
578         #Create a tpfile if one doesn't exist
579     if not localtpfile:
580         #storkutil.makeTPFile(storkusername, False)
581         storkutil.makeTPFile(storkusername, True) # createblank set to True for testing no default key trusted
582         arizonacrypt.XML_sign_file_using_privatekey_fn(storkusername+'.tpfile', privatekey.file, privatekey.password)
583         storkutil.pubKeyEmbed(storkusername+'.tpfile', publickey.file)
584         os.remove(storkusername+'.tpfile')
585         localtpfile = storkusername+'.'+getPubKeyHash()+'.tpfile'
586     
587     # download a fresh config file if we don't have any at all
588     if not unsignedconffile:
589        localconffile = sc.fetch_configuration(topoptions.get_slice(), defaultconf=True)
590        if not localconffile:
591            #arizonareport.send_out(2, "Could not fetch default configuration file.")
592            raise StorkGuiException, " Could not fetch a configuration file, even a default one."
593        extractSignedFile(localconffile, unsignedconffile)
594         
595     # change the configuration file
596     #pubkey (last argument) probably isn't needed to be passed along anymore, alter_configuration will need to have signature changed
597     if not sc.alter_configuration(unsignedconffile, storkusername, "", True):
598         arizonareport.send_error(1, "[ERROR] There was an error changing the configuration file.")
599        #if we are about to change some values then
600        #warn the user here with a pop-up, and ask them wether to try again, this time overwriting
601        #any values
602
603 #       (susername, pk) = sc.parse_config(localconffile)
604 #       if susername == None: susername = "unknown"
605 #       #if pk       == None: pk       = "unknown"
606 #       
607 #       # the key is only being compared in name
608 #       #if susername == 'default' and pk == 'default.publickey':
609 #       if susername == 'default':
610 #           changeidentity = True
611 #       elif storkusername != susername:
612 #           changeidentity = askyesno('Verify', "Slice '"+topoptions.get_slice()+"' \nis currently managed by \nuser '"+susername+"'.\n\nDo you want to change it to your current user '"+storkusername+"'?")
613 #       #else:
614 #       #    changeidentity = askyesno('Verify', "Slice '"+topoptions.get_slice()+"' \ncurrently uses public key '"+pk+"'.\n\nDo you want to change it to your current public key '"+pubkey+"'?") 
615 #       if changeidentity:
616 #          #pubkey (3rd argument) probably isn't needed to be passed along anymore, alter_configuration will need to have signature changed
617 #          if not sc.alter_configuration(localconffile, storkusername, "", True):
618 #              arizonareport.send_error(1, "[ERROR] There was an error changing the configuration file.")
619
620     # clean up and make any other required changes to the configuration file
621     if not sc.clean_configuration(unsignedconffile):
622         arizonareport.send_error(1, "[ERROR] There was an error cleaning the configuration file.")
623         
624     # create a signed conf file
625     localconffile = createSignedConfFile(unsignedconffile)
626         
627     # upload files
628     if not debug:
629        showwarning("About to upload new files", "About to upload new files to the repository. This may take a while. During the upload, the GUI will be unresponsive.")
630        sc.upload_file(username, password, localgroupsfile, "pacman")
631        sc.upload_file(username, password, localpackagesfile, "pacman")
632        sc.upload_file(username, password, localtpfile, "tp")
633        sc.upload_file(username, password, localconffile, "conf", topoptions.get_slice())
634
635        # upload any packages we need to
636        if len(to_upload) > 0:
637            short = []
638            for foo in to_upload: 
639               short.append( os.path.basename(foo) )
640            for foo in to_upload:
641               uploadPackage(foo)
642
643        # call the auto update page with a list of all the possible nodes to add to this slice
644        nodestoadd = []
645        for group in groups:
646           for node in nodes[group]:
647              if node not in nodestoadd:
648                 nodestoadd.append(node)
649
650        sc.autoSetup(username,password,topoptions.get_slice(),nodestoadd,wheretologin); 
651
652
653 nodes["All"] = []
654 actions["All"] = []
655
656
657 def setSlice(slicename):
658     """This is called when the user selects a slice in the GUI's dropdown menu. This
659        will make sure everything that needs to be changed is changed. Currently
660        it will set the 'username' arizonaconfig option, change the flags we pass
661        to all of the storkutil command line calls made here within this module,
662        and update the keys that are considered administrator keys for the slice.
663     """
664     
665     global storkusername
666     global storkutiluserflags
667     global publickey
668     global privatekey
669     
670     arizonareport.send_out(3, "Setting slice to: " + str(storkusername))
671     storkusername = slicename
672     arizonaconfig.set_option("username", storkusername)
673     storkutiluserflags = '--username='+storkusername+' --publickey='+publickey.file+' --privatekey='+privatekey.file
674     
675     # update the keys we consider administrators for the slice
676     getSliceKeys(slicename)
677
678
679 def updateFromRepository():
680      """Downloads all of the latest files from the repository.
681         We'll need these files to determine which tpfile and pacman files to use.
682      """
683    
684      global localconffile
685      global localtpfile
686      global localpackagesfile
687      global localgroupsfile
688     
689      global unsignedconffile
690      global unsignedtpfile
691      global unsignedpackagesfile
692      global unsignedgroupsfile
693
694      # clear the key database as its old value will be reused if we don't
695      storkusernamepackage.reset_key_database()
696
697      # clear what is displayed in the gui
698      depopulate()
699      
700      # make sure the latest files are downloaded from the repository
701      storkpackagelist.init()
702    
703      # for each type of file, find the latest in the repository that is signed
704      # by an administrator of this slice
705      result = storkpackagelist.find_file_kind("pacman", "packages.pacman")
706      if result and result[0] != None:
707          (unsignedpackagesfile, localpackagesfile) = createLocalFileFromRepoFile("packages", result[0])
708      else:
709          (unsignedpackagesfile, localpackagesfile) = (None, None)
710          
711      result = storkpackagelist.find_file_kind("pacman", "groups.pacman")
712      if result and result[0] != None:
713          (unsignedgroupsfile, localgroupsfile) = createLocalFileFromRepoFile("groups", result[0])
714      else:
715          (unsignedgroupsfile, localgroupsfile) = (None, None)
716          
717      result = storkpackagelist.find_file_kind("tpfiles", "tpfile")
718      if result and result[0] != None:
719          (unsignedtpfile, localtpfile) = createLocalFileFromRepoFile("tpfile", result[0])
720      else:
721          (unsignedtpfile, localtpfile) = (None, None)
722          
723      result = storkpackagelist.find_file_kind("conf", "stork.conf")
724      if result and result[0] != None:
725          (unsignedconffile, localconffile) = createLocalFileFromRepoFile("conf", result[0])
726      else:
727          (unsignedconffile, localconffile) = (None, None)
728          
729      # refresh what the gui displays
730      populate()
731      
732      # we should be showing what's current in the repository at this point
733      global statusframe
734      statusframe.set_in_synch(True, upload_files=False)
735
736
737 def createSignedConfFile(unsigned_file):
738     """Takes the to the unsigned file and creates a file in the current directory
739        called [storkusername].[pubkeyhash].stork.conf that is a signed version of
740        the same file.
741     """
742     
743     global storkusername
744     global publickey
745     signed_file = storkusername + "." + publickey.hash + ".stork.conf"
746
747     copyFile(unsigned_file, signed_file)
748     signFile(signed_file)
749     
750     #TODO should check if error occurrsed in signing
751     
752     return signed_file
753
754
755 def depopulate():
756
757      global topblock
758      topblock.remove_all_groups()
759
760
761 def populate():
762
763      #initialize the nodes/groups/packages
764      global groups
765      global nodes
766      global actions
767      global topblock
768      global frm
769      
770      groups = getGroups()
771      for group in groups:
772         nodes[group] = getNodes(group)
773         actions[group] = getActions(group)
774
775      group = Group(frm, "All", topblock)
776      group.grid(pady=3, columnspan=2, sticky=NW)
777      groupframes["All"] = group
778     
779      for groupname in groups:
780         if groupname == "All": continue
781         group = Group(frm, groupname, topblock)
782         group.config(relief=GROOVE)
783         group.grid(pady=3,columnspan=2, sticky=NW) 
784         groupframes[groupname] = group
785     
786      groups.append("All")
787
788
789 class StatusFrame(Frame):
790     
791    def __init__(self, parent=None):
792       Frame.__init__(self, parent)
793       self.parent = parent
794
795       self.goodimage = PhotoImage(file=os.path.join(imagepath, "in-synch-1.gif"))   
796       #self.badimage  = PhotoImage(file=os.path.join(imagepath, "out-of-synch-1.gif"))   
797       self.upload    = PhotoImage(file=os.path.join(imagepath, "upload-1.gif"))   
798
799       self.helv36 = tkFont.Font ( family="Helvetica",\
800         size=14, weight="bold" )
801
802       self.helv10 = tkFont.Font ( family="Helvetica",\
803         size=10 )
804
805       self.helv9 = tkFont.Font ( family="Helvetica",\
806         size=9, )
807
808       self.linktext = tkFont.Font ( family="Helvetica",\
809         size=9, underline=1, weight=tkFont.BOLD)
810
811       self.statusimage = Label(self, image=self.goodimage)
812
813       self.statustext  = Label(self, text="In synch with repository.")
814       self.uploadtext  = Label(self, text="Files added or changed.\nSynch with repository.", fg="blue")
815       self.uploadimage = Button(self, image=self.upload, command=lambda: self.set_in_synch(True) )
816
817       #self.statusimage.bind("<Button-1>", lambda event: self.set_in_synch(False) )
818       self.uploadtext.bind("<Button-1>", lambda event: self.set_in_synch(True))
819       
820       global synched
821       if synched:
822           self.statustext.grid(row=1, column=0, sticky=E)
823           self.statusimage.grid(row=1, column=1, sticky=E)
824       else:
825          self.uploadtext.grid(row=0, column=0, sticky=E)
826          self.uploadimage.grid(row=0, column=1, sticky=E) 
827
828
829    def set_in_synch(self, new_synched_status, upload_files=True):
830       global synched
831       if new_synched_status:
832          # if already synched, let user know no synch necessary
833          if synched == True and upload_files:
834             showwarning("No synch necessary", "You are already synched with the repository.")
835          else:
836             if not topoptions.get_slice() and upload_files:
837                showwarning("Select slice", "You must select a slice before synchronizing with the repository.")
838                return
839             if upload_files:
840                 createFiles()
841             self.uploadtext.grid_forget()
842             self.uploadimage.grid_forget()
843             self.statustext.grid(row=0, column=0, sticky=E)
844             self.statusimage.grid(row=0, column=1, sticky=E)
845       else:
846          self.statustext.grid_forget()
847          self.statusimage.grid_forget()
848          self.uploadtext.grid(row=0, column=0, sticky=E)
849          self.uploadimage.grid(row=0, column=1, sticky=E)
850       synched = new_synched_status
851      
852
853 class TopOptions(Frame):
854
855    def __init__(self, parent=None, root=None):
856       Frame.__init__(self, parent)
857       self.root = root
858       self.helv36 = tkFont.Font ( family="Helvetica",\
859         size=14, weight="bold" )
860
861       #self.userimage = PhotoImage(file=os.path.join(imagepath, "user-1.gif"))
862
863       managingslice_label = Label(self, text="is managing") 
864       self.username       = Label(self, text="",font=self.helv36)
865       if username:
866          self.username.config(text=username)
867
868       self.username.bind("Button-1", lambda event: self.quit() )
869
870       #self.changeframe = Frame(self)
871       #self.changetext  = Label(self.changeframe, text="Switch User")
872       #self.changeimage = Button(self.changeframe, image=self.userimage, command=self.switchuser)
873
874       self.slicevar       = StringVar()
875       self.slicemenu      = OptionMenu(self, self.slicevar, None)
876       self.slicemenu["menu"].config(font=self.helv36)
877       self.slicemenu["menu"].delete(0, END)
878       
879       slices = getSlices()
880       #if len(slices)>0:
881       #   self.slicevar.set(slices[0])
882       self.selectsliceprompttext = "[select slice]"
883       self.slicevar.set(self.selectsliceprompttext)
884       
885       def set_active_slice(who_knows_maybe_some_tkinter_thing):
886          global synched
887          global storkusername
888          
889          # check if they didn't actually change what was selected
890          if storkusername == self.get_slice():
891              return
892          
893          # if they have changes that will be lost, give them a chance to abort the change
894          if synched or askokcancel("Change slice without synching?", "You have changes that are not synched with the repository. These changes will be lost if you change the slice right now. \n\nClick OK to change slice without synching.", default=CANCEL):
895              setSlice(self.get_slice())
896              updateFromRepository()
897          else:
898              # they decided not to change the slice, so set the select box back to what it was
899              self.slicevar.set(storkusername)
900       
901       for slicename in slices:
902          self.slicemenu["menu"].insert('end','command', label=slicename, command=_setit(self.slicevar, slicename, set_active_slice)) 
903       
904       self.username.grid(row=0, column=0, sticky=W)
905       managingslice_label.grid(row=0, column=1, sticky=E)
906       self.slicemenu.grid(row=0, column=2, sticky=W)
907       #self.changetext.grid(row=0, column=0,sticky=W)
908       #self.changeimage.grid(row=0, column=1) 
909       #self.changeframe.grid(row=1, column=2, sticky=E)
910
911    
912    def get_slice(self):
913       """Returns the name of the slice that is currently selected in the dropdown
914          menu, or None if no slice is selected.
915       """
916       if self.slicevar.get() == self.selectsliceprompttext:
917           return None
918       else:
919           return self.slicevar.get()
920
921
922    #def switchuser(self):
923    #   """Supposed clear all state and reset the application. This never really worked
924    #      and I think it's a bit useless compared with other more pressing needs and
925    #      the importance of a bug-free gui, so removed by jsamuel."""
926    #      
927    #   global statusframe
928    #   statusframe.set_in_synch(False)
929    #   #TODO -actually clear the state out
930    #   global switch_user
931    #   switch_user = True
932    #   arizonareport.send_out(4, "About to switch user to: " + str(switch_user))
933    #   close_window_callback()
934
935
936 class TopBlock(Frame):
937    def __init__(self, parent=None):
938       Frame.__init__(self, parent)
939       self.parent=parent
940       self.linktext = tkFont.Font ( family="Helvetica",\
941         size=9, underline=1, weight=tkFont.BOLD)
942       self.helv10 = tkFont.Font ( family="Helvetica",\
943         size=10, weight="bold" )
944
945       self.var = StringVar()
946
947       #addnew   = Label(self, text="Add/Remove group: ")
948       self.newfield = Entry(self, textvariable=self.var)
949       self.add_group= Button(self, text="Ok", font=self.helv10, command=self.add_group )
950       self.cancel   = Button(self, text="Cancel", font=self.helv10, command=self.hide_addfields)
951       self.newfield.bind("<Return>", lambda event: self.add_group.invoke() )
952
953       self.addgroup_label = Label(self, text="add group", font=self.linktext, fg="blue", cursor="hand2")
954       self.addgroup_label.bind("<Button-1>", lambda event: self.show_addfields() )
955       self.addgroup_label.grid(column=0, row=0, sticky=NW) 
956
957
958    def hide_addfields(self):
959       self.addgroup_label.grid(column=0, row=0, sticky=NW)
960       self.newfield.grid_forget()
961       self.add_group.grid_forget()
962       self.cancel.grid_forget()
963
964
965    def show_addfields(self):
966       # only let them add groups if they have selected a slice
967       # that is, when the gui first starts up no slice is selected, and it was easier
968       # to do this then to try to make this only show up after they selected a slice
969       global storkusername
970       if not storkusername:
971           showwarning("Please select a slice", "You must select a slice before you can add groups.")
972           return
973        
974       self.addgroup_label.grid_forget()    
975       self.newfield.grid(column=0, row=0, sticky=W)
976       self.add_group.grid(column=1, row=0, sticky=W)
977       self.cancel.grid(column=2, row=0, sticky=W)
978       self.var.set("")
979       self.newfield.focus()
980
981
982    def add_group(self) :
983       group = self.newfield.get().strip()
984       if len(group) == 0: return
985       if re.search(r"[^a-zA-Z0-9_-]", group):
986           showwarning("Invalid group name", "Group names can only contain the characters a-z, A-Z, 0-9, underscores, and hyphens.")
987           return
988
989       if group not in groups:
990          groups.append(group)
991          nodes[group] = []
992          actions[group] = []
993          #create a new group
994          newgroup = Group(self.parent, group, self)
995          newgroup.grid(pady=3,columnspan=2, sticky=W)
996          groupframes[group]=newgroup
997          self.set_group(group)
998          self.grid_forget()
999          self.grid(column=0,pady=10, sticky=NW)
1000          global statusframe
1001          statusframe.set_in_synch(False)
1002          
1003       self.hide_addfields()
1004
1005
1006    def remove_group(self):
1007       if self.var.get() != None and self.var.get() != "":
1008          # make sure the thing we are trying to move actually exists
1009          toremove = self.var.get()
1010          if toremove not in groups: return
1011
1012          # remove it
1013          groupframes[toremove].destroy()
1014          groups.remove(toremove)
1015          nodes[toremove] = []
1016
1017          if len(groups) > 0:
1018             self.set_group( groups[-1] )
1019          else:
1020             self.set_group("")
1021             
1022          global statusframe
1023          statusframe.set_in_synch(False)
1024
1025
1026    def remove_all_groups(self):
1027        arizonareport.send_out(3, "Removing all groups.")
1028        while len(groups):
1029            arizonareport.send_out(4, "Removing group: " + str(groups[0]))
1030            self.set_group(groups[0])
1031            self.remove_group()
1032
1033
1034    def set_group(self, groupname):
1035       if groupname != None:
1036          self.var.set(groupname)
1037
1038
1039 class Group(Frame):
1040     
1041    nodelist = [] 
1042    actions  = []
1043    removenow= (None, None) # the event handler for the node remove button will store its parameters in here
1044    tempfile = None
1045
1046    def __init__(self, parent=None, groupname=None, top=None ):
1047       Frame.__init__(self, parent)
1048       self.collapsed=1
1049       self.top = top
1050       self.group_name = groupname
1051       self.next_action_row = 0
1052       self.config(bd=2, relief=RIDGE)
1053       
1054       self.ximage = PhotoImage(file=os.path.join(imagepath, "xbutton-1.gif"))   
1055       #self.addimage=PhotoImage(file=os.path.join(imagepath, "addbutton-1.gif"))
1056
1057       self.helv36 = tkFont.Font ( family="Helvetica",\
1058         size=14, weight="bold" )
1059
1060       self.helv10 = tkFont.Font ( family="Helvetica",\
1061         size=10 )
1062
1063       self.helv9 = tkFont.Font ( family="Helvetica",\
1064         size=9, )
1065
1066       self.linktext = tkFont.Font ( family="Helvetica",\
1067         size=9, underline=1, weight=tkFont.BOLD)
1068
1069
1070       nodelist = nodes[groupname]
1071       arizonareport.send_out(4, "nodelist for group " + str(groupname) + ": " +  str(nodelist))
1072       if nodelist != None:
1073          numnodes = str( len(nodelist) )
1074       else:
1075          nodelist = []
1076          numnodes = "0"
1077
1078       actionlist = actions[groupname]
1079       arizonareport.send_out(4, "actionlist for group " + str(groupname) + ": " +  str(actionlist))
1080       if actionlist == None:
1081          actionlist = []
1082       
1083
1084       if groupname == None:
1085          groupname = "None"
1086
1087       group_label_frame = Frame(self)
1088       inner_group_label_frame = Frame(group_label_frame)
1089       group_label_frame.grid(row=0, column=0, rowspan=2,  sticky=NW)
1090
1091      
1092       remove_group = Button(inner_group_label_frame, image=self.ximage,borderwidth=0, command=self.remove_group)
1093       self.group= Label(inner_group_label_frame, text=groupname+" ("+numnodes+")", width=30, anchor=W, font=self.helv36)
1094       if groupname == "All":
1095          self.group.config(text=groupname+" nodes")
1096
1097       if groupname != "All":
1098          remove_group.grid(column=0, row=0, sticky=W)
1099       
1100       self.group.grid(column=1, row=0, sticky=W )
1101
1102       inner_group_label_frame.grid(row=0, column=0, sticky=W)
1103
1104       self.expandgroup = Label(group_label_frame, text="expand", font=self.linktext, fg="blue", cursor="hand2")
1105       if groupname != "All":
1106          self.expandgroup.grid(column=0, row=1, sticky=NW)
1107          self.expandgroup.bind('<Button-1>', self.expand_group) 
1108
1109       #self.sbar = Scrollbar(self)
1110       #self.lbox = Listbox(self, height=5, width=40, relief=SUNKEN)
1111       self.lbox = Frame(group_label_frame, width=40, relief=GROOVE)
1112
1113       #construct the box for adding new nodes
1114       self.addbox = Frame(group_label_frame, bg="#7AFFA4")
1115       self.add_label    = Label(self.addbox, text="Add:", font=self.helv10, bg="#7AFFA4")
1116       self.addmethodvar = StringVar()
1117       self.addmethodvar.set("single node")
1118       self.optionmenu = OptionMenu(self.addbox, self.addmethodvar, "single node", "CoMon query", "set operation")
1119
1120       def onchange(name, index, mode):
1121          if self.addmethodvar.get() == "single node":
1122             self.use_singlenode()
1123          elif self.addmethodvar.get() == "CoMon query":
1124             self.use_comon()
1125          elif self.addmethodvar.get() == "set operation":
1126             self.use_set()
1127
1128       remember = self.addmethodvar.trace_variable('w', onchange)
1129       #self.optionmenu["menu"].config(command=onchange)
1130
1131       self.singlenode_ex = Label(self.addbox, justify=LEFT,width=35,height=2, text="Type in the hostname of a node in \nthe box, ex: planetlab-1.cs.princeton.edu", font=self.helv9, bg="#7AFFA4")
1132       #self.comon_ex      = Label(self.addbox, justify=LEFT,width=50,height=3, text="Type in a well formed CoMon query to return a set of nodes. \nWARNING: the nodes returned from the query will \nreplace any nodes that are currently in this group.", font=self.helv9, bg="#7AFFA4")
1133       #self.set_ex        = Label(self.addbox, justify=LEFT,width=25,height=2, text="Union or Intersect this group \nwith another existing group.", font=self.helv9, bg="#7AFFA4")
1134       self.comon_ex      = Label(self.addbox, justify=LEFT,width=50,height=3, text="Comon support is currently not functional", font=self.helv9, bg="#666666")
1135       self.set_ex        = Label(self.addbox, justify=LEFT,width=50,height=3, text="Set support is currently not functional", font=self.helv9, bg="#666666")
1136
1137
1138       self.node_entry    = Entry(self.addbox, width=25 )
1139       self.node_label    = Label(self.addbox, text="Node:", font=self.helv10, bg="#7AFFA4")
1140
1141       self.comon_query   = Text(self.addbox, height=4, width=30 , relief=SUNKEN)
1142       self.comon_label   = Label(self.addbox, text="Query:", font=self.helv10, bg="#7AFFA4")
1143
1144       self.set_label     = Label(self.addbox, text="Operation:", font=self.helv10, bg="#7AFFA4")
1145       self.set_option    = StringVar()
1146       self.set_option.set("intersect")
1147       self.set_menu      = OptionMenu(self.addbox, self.set_option, "intersect", "union")
1148       self.set_with      = Label(self.addbox, text="With", font=self.helv10, bg="#7AFFA4")
1149       self.set_groupvar   = StringVar()
1150       self.set_grouplist  = OptionMenu(self.addbox, self.set_groupvar, None)
1151       self.set_grouplist["menu"].delete(0, END)
1152       self.add_label.grid(row=0, column=0, sticky=NE)
1153       self.optionmenu.grid(row=0, column=1,sticky=NW)
1154       self.singlenode_ex.grid(row=1, column=1,sticky=NW)
1155       self.node_label.grid(row=2, column=0, sticky=NE)
1156       self.node_entry.grid(row=2, column=1, sticky=NW)
1157
1158       self.node_buttons = Frame(self.addbox, bg="#7AFFA4")
1159       self.node_ok        = Button(self.node_buttons, text="Ok", font=self.helv9,  command=self.add_node)
1160       self.node_cancel    = Button(self.node_buttons, text="Cancel", font=self.helv9,  command=self.hide_addnodes)
1161       self.node_entry.bind("<Return>", lambda event: self.node_ok.invoke() ) 
1162
1163
1164       self.node_cancel.grid(row=0, column=0, sticky=NE)
1165       self.node_ok.grid(row=0, column=1, sticky=NE)
1166       self.node_buttons.grid(row=3, column=1, sticky=NE)
1167       
1168      
1169       self.addnode_label = Label(group_label_frame, text="add node(s)", font=self.linktext, fg="blue", cursor="hand2")
1170       self.addnode_label.bind("<Button-1>", lambda event: self.show_addnodes() )
1171       if groupname != "All":
1172          self.addnode_label.grid(row=3, column=0, sticky=NW)
1173
1174
1175       self.actionframe = Frame(self)
1176       self.action=Label(self.actionframe, text="Actions",width=38,anchor=W, font=self.helv36)
1177       self.action.grid(column=0, row=0,columnspan=2,  padx=1, sticky=NW)
1178
1179
1180       self.actionbox = Frame(self.actionframe, width=39, relief=SUNKEN)
1181       self.actionbox.grid( column=0, row=1, rowspan=1, sticky=NW )
1182
1183       self.actionframe.grid(column=1, row=0, sticky=NW )
1184
1185       for i,foo in enumerate(nodelist):
1186          n = Label(self.lbox,text=foo, font=self.helv9)
1187          x = Button(self.lbox, image=self.ximage, borderwidth=0 )
1188          x['command'] = lambda param=(x,n): self.remove_node(param)
1189
1190          self.nodelist.append(n)
1191          x.grid(column=0, row=i, sticky=W, pady=0 ) 
1192          n.grid(column=1, row=i, sticky=W, pady=0 ) 
1193
1194       for foo in actionlist:
1195          n = Label(self.actionbox, text="    "+foo[0]+" ("+foo[1]+")", font=self.helv9, anchor=W)
1196          x = Button(self.actionbox, image=self.ximage, borderwidth=0)
1197          x["command"]= lambda param=(n,x,foo): self.remove_action(param)
1198          self.actions.append(n)
1199          x.grid(column=0, row=self.next_action_row, padx=0,  sticky=NW)
1200          n.grid(column=1, row=self.next_action_row, padx=0,  sticky=NW)
1201          self.next_action_row += 1
1202
1203       #create the addaction button
1204       self.add_action_area = Frame(self.actionbox, bg="#7AFFA4", relief=SUNKEN)
1205       self.add_action_label = Label( self.actionbox, text="add action", font=self.linktext, fg="blue", cursor="hand2")
1206       self.add_action_label.bind("<Button-1>", lambda event: self.show_actionbuttons() )
1207
1208       self.action_explenation = Label( self.add_action_area,  text="Either type the name of a package in the text box\nor browse for a package on your filesystem.", font=self.helv9, fg="black", height=2, bg="#7AFFA4")
1209
1210       self.var = StringVar()
1211       self.var.set("install")
1212       self.optionmenu = OptionMenu(self.add_action_area, self.var, "install", "update", "remove")
1213
1214
1215       self.action_var= StringVar()
1216       self.add_action= Entry(self.add_action_area,textvariable=self.action_var, width=22, relief=SUNKEN)
1217       self.add_action.bind("<Return>", lambda event: self.add_action_to_list() )
1218       self.browse    = Button(self.add_action_area,text="Browse...", font=self.helv10, command=self.browse_for_file)
1219
1220       self.cancel    = Button(self.add_action_area,text="Cancel", font=self.helv10, command=lambda: self.hide_actionbuttons() )
1221       self.ok        = Button(self.add_action_area,text="Ok", font=self.helv10,command=lambda: self.add_action_to_list() )
1222       
1223       self.action_explenation.grid(column=0, row=0,columnspan=2,  sticky=NW)
1224       self.add_action.grid(column=0, row=2, sticky=NW)
1225       self.browse.grid(column=1,  row=2, sticky=NW)
1226       self.optionmenu.grid(column=0, row=3, sticky=NE)
1227       self.cancel.grid(column=0, row=4, sticky=NE)
1228       self.ok.grid(column=1, row=4, sticky=NW)
1229       
1230       self.add_action_label.grid(column=0, columnspan=2, sticky=W)
1231
1232
1233    def browse_for_file(self):
1234       filename = tkFileDialog.askopenfilename()
1235       if not filename: return
1236       if not isPackage(filename):
1237             showerror("Package not understood", "The package must be an rpm or a tar.")
1238             self.action_var.set("")
1239             return
1240
1241       arizonareport.send_out(4, "Selected file: " + str(filename))
1242       if (self.var.get() == "update" or self.var.get() == "install") and os.path.isfile(filename):
1243          self.tempfile = filename
1244          self.action_var.set( os.path.basename( filename ) )
1245
1246
1247    def show_addnodes(self):
1248       self.addnode_label.grid_forget()
1249       self.addbox.grid(row=4,column=0, sticky=NW)
1250       self.addmethodvar.set("single node")
1251       self.node_entry.delete(0, len(self.node_entry.get()))
1252       #self.use_singlenode()
1253
1254
1255    def hide_addnodes(self):
1256       self.addbox.grid_forget()
1257       self.addnode_label.grid(row=3,column=0, sticky=NW)
1258
1259
1260    def use_singlenode(self):
1261       self.node_buttons.grid_forget()
1262       self.comon_ex.grid_forget()
1263       self.comon_label.grid_forget()
1264       self.comon_query.grid_forget()
1265
1266       self.set_ex.grid_forget()
1267       self.set_label.grid_forget()
1268       self.set_menu.grid_forget()
1269       self.set_with.grid_forget()
1270       self.set_grouplist.grid_forget()     
1271
1272       self.singlenode_ex.grid(row=1, column=1,sticky=NW)
1273       self.node_label.grid(row=2, column=0, sticky=NE)
1274       self.node_entry.grid(row=2, column=1, sticky=NW)
1275       self.node_cancel.grid(row=3, column=0, sticky=NE)
1276       self.node_ok.grid(row=3, column=1, sticky=NE)
1277       self.node_buttons.grid(row=3, column=1, sticky=NE)
1278       self.node_entry.focus() 
1279   
1280   
1281    def use_comon(self):
1282       self.node_buttons.grid_forget()
1283       self.singlenode_ex.grid_forget()
1284       self.node_label.grid_forget()
1285       self.node_entry.grid_forget()
1286
1287       self.set_ex.grid_forget()
1288       self.set_label.grid_forget()
1289       self.set_menu.grid_forget()
1290       self.set_with.grid_forget()
1291       self.set_grouplist.grid_forget()     
1292
1293       self.comon_ex.grid(row=1, column=1, sticky=NW)
1294       self.comon_label.grid(row=2, column=0, sticky=NE)
1295       self.comon_query.grid(row=2, column=1, sticky=NW)
1296       self.node_buttons.grid(row=3, column=1, sticky=NE)
1297       self.comon_query.focus()
1298
1299
1300    def use_set(self):
1301       self.node_buttons.grid_forget()
1302       self.singlenode_ex.grid_forget()
1303       self.node_label.grid_forget()
1304       self.node_entry.grid_forget()
1305
1306       self.comon_ex.grid_forget()
1307       self.comon_label.grid_forget()
1308       self.comon_query.grid_forget()
1309
1310       self.set_ex.grid(row=1, column=1,  sticky=NW)
1311       self.set_label.grid(row=2, column=0, sticky=NE)
1312       self.set_menu.grid(row=2, column=1,sticky=NW)
1313       self.set_with.grid(row=3, column=0, sticky=NE)
1314       self.set_grouplist.grid(row=3, column=1, sticky=NW)
1315       self.node_buttons.grid(row=4, column=1, sticky=NE)
1316
1317       try:
1318          self.set_grouplist["menu"].delete(0,END)
1319       except: pass
1320
1321       def nothing(junk): pass
1322
1323       for foo in groups:
1324          if foo == 'All': continue
1325          if foo!=self.group_name:
1326             self.set_grouplist["menu"].insert('end','command', label=foo, command=_setit(self.set_groupvar, foo, nothing)) 
1327             self.set_groupvar.set(foo)
1328       
1329    
1330
1331    def add_node(self):
1332       """this function will add a node to the current group,
1333          how that node or nodes is/are added depends on the
1334          method the user choose, eg: single node, comon, set op
1335       """
1336    
1337       method = self.addmethodvar.get()
1338
1339       if method == "single node":
1340          node = self.node_entry.get().strip()
1341          if len(node) == 0: return
1342          # TODO separately check valid for hostname, IPv4, or IPv6
1343          if re.search(r"[^a-zA-Z0-9:.-]", node):
1344             showwarning("Invalid group name", "Node names can only contain the characters a-z, A-Z, 0-9, periods, hyphens, and colons.")
1345             return
1346          arizonareport.send_out(4, "About to add single node: " + str(node))
1347          if node != None and node != "" and node not in nodes[self.group_name]: 
1348             # add this node to the group
1349             i = len(nodes[self.group_name])
1350             nodes[self.group_name].append(node)
1351
1352             n = Label(self.lbox,text=node, font=self.helv9)
1353             x = Button(self.lbox, image=self.ximage, borderwidth=0 )
1354             x['command'] = lambda param=(x,n): self.remove_node(param)
1355             
1356             self.nodelist.append(n)
1357             x.grid(column=0, row=i, sticky=W, pady=0 ) 
1358             n.grid(column=1, row=i, sticky=W, pady=0 ) 
1359             #update label
1360             self.group.config(text=self.group_name+" ("+str(len(nodes[self.group_name]))+")" )
1361             global statusframe
1362             statusframe.set_in_synch(False)
1363
1364          pass #TODO stub
1365       elif method == "CoMon query":
1366          pass #TODO stub
1367       elif method == "set operation":
1368          pass #TODO stub
1369
1370       self.hide_addnodes(); 
1371
1372
1373    def hide_actionbuttons(self):
1374       self.add_action_area.grid_forget()
1375       self.add_action_label.grid(column=0)
1376
1377        
1378    def show_actionbuttons(self):
1379       self.add_action_label.grid_forget()
1380       self.add_action_area.grid(column=0, columnspan=2 )
1381       self.add_action.focus()
1382       self.add_action.delete(0, len(self.add_action.get()))
1383
1384
1385    def add_action_to_list(self):
1386        #param is (package, type)
1387        param = ( self.add_action.get().strip(), self.var.get() )
1388        if len(param[0]) == 0:
1389           return
1390        for existingparam in actions[self.group_name]:
1391           if param[0] == existingparam[0]:
1392              showwarning("Action already exists", "An action already exists for this package. To change the action for this package, remove the current action and then add the new one.")
1393              return
1394        if re.search(r"[^a-zA-Z0-9._-]", param[0]):
1395           showwarning("Invalid package name", "Package names can only contain the characters a-z, A-Z, 0-9, periods, underscores, and hyphens.")
1396           return
1397       
1398        arizonareport.send_out(3, "addactin param=" + str(param))
1399        arizonareport.send_out(4, "actions[self.group_name]: " + str(actions[self.group_name]))
1400        arizonareport.send_out(4, "going to add new action to row=" + str(self.next_action_row))
1401
1402        n = Label(self.actionbox, text="    "+param[0]+" ("+param[1]+")", font=self.helv9)
1403        x = Button(self.actionbox, image=self.ximage, borderwidth=0)
1404        x["command"]= lambda param=(n,x,param): self.remove_action(param)
1405        self.actions.append(n)
1406        x.grid(column=0, row=self.next_action_row, sticky=NW)
1407        n.grid(column=1, row=self.next_action_row, sticky=NW)
1408        self.next_action_row += 1
1409
1410        actions[self.group_name].append( param )
1411        self.hide_actionbuttons()
1412        global statusframe
1413        statusframe.set_in_synch(False)
1414     
1415        # see if we have to add this to the trusted packages file (that is,
1416        # if the user just selected a package on their file system
1417        if self.tempfile != None:
1418          global to_upload
1419          if self.tempfile not in to_upload:
1420             trustPackage(self.tempfile)
1421             to_upload.append(self.tempfile) 
1422
1423        self.tempfile = None
1424        
1425
1426    def remove_action(self, param):
1427       self.actions.remove(param[0])
1428       param[0].destroy()
1429       param[1].destroy()
1430       #TODO Error, the tuple is not being removed from the action list correctly
1431       arizonareport.send_out(3, "remove_action param=" + str(param))
1432
1433       actiontuple = param[2]
1434       for foo in actions[self.group_name]:
1435          if foo[0] == actiontuple[0] and foo[1] == actiontuple[1]:
1436             arizonareport.send_out(4, "Removing action: " + str(foo))
1437             actions[self.group_name].remove(foo)
1438             global statusframe
1439             statusframe.set_in_synch(False)
1440
1441
1442    def remove_node(self, param):
1443       node = param[1]["text"]
1444       if node in nodes[self.group_name]:
1445          nodes[self.group_name].remove(node)
1446       param[0].destroy()
1447       param[1].grid_forget()
1448       param[1].destroy()
1449       self.group.config(text=self.group_name+" ("+str(len(nodes[self.group_name]))+")" )
1450       global statusframe
1451       statusframe.set_in_synch(False)
1452       
1453       
1454    def collapse_group(self, event):
1455       #do everything needed to make most items "hidden"
1456       if self.collapsed: return
1457
1458       self.lbox.grid_forget()
1459       
1460       self.collapsed = 1       
1461       self.expandgroup.bind('<Button-1>', self.expand_group)
1462       self.expandgroup.config(text="expand") 
1463   
1464
1465    def expand_group(self, event):
1466       #do everything needed to make things visible again
1467       if not self.collapsed: return
1468
1469       self.lbox.grid( column=0, row=2, sticky='nw', pady=0 ) 
1470
1471       self.collapsed = 0
1472       self.expandgroup.bind('<Button-1>', self.collapse_group )
1473       self.expandgroup.config(text="collapse")
1474
1475
1476    def remove_group(self):
1477       #TODO put hook into storkutil to remove this group
1478       groups.remove(self.group_name)
1479       nodes[self.group_name] = []
1480       self.destroy()
1481       global statusframe
1482       statusframe.set_in_synch(False)
1483
1484
1485 def authenticate(username, password,site):
1486    """Attempt to login to the repository with the provided user/pass."""
1487    
1488    return sc.login(username, password,site)
1489  
1490
1491 # initially the user is not authenticated
1492 authenticated = False
1493
1494 def makeloginwindow():
1495    """Displays the window that prompts for PL/repo login info and
1496       selection of private key file."""
1497       
1498    login = Tk()
1499    login.title('Stork Slice Manager')
1500    login.width=1100
1501    login.height=800
1502    helv10 = tkFont.Font ( family="Helvetica",\
1503         size=10, weight="bold" )
1504    
1505    text_content = "Please enter your Planetlab username and password. \n"
1506    text_content += "This information will only be transmitted over a secure connection \n"
1507    text_content += "and is needed to identify the slices you have access to.\n"
1508    text_content += "\n"
1509    text_content += "Your PlanetLab key is also needed in order to sign files that will be\n"
1510    text_content += "uploaded to the Stork repository."
1511    text_content += "\n"
1512    ex_label       = Label(login,width=60, height=8, justify=LEFT, text=text_content)
1513
1514    username_label = Label(login,width=20,text="Planetlab Username:")
1515    password_label = Label(login,width=20,text="Planetlab Password:")
1516    storkusername_label = Label(login,width=20,text="Slice Name:")
1517    
1518    privatekeyfile_label = Label(login,width=20,text="Private key:")
1519    privatekeypassword_label = Label(login,width=20,text="Private key password:")
1520
1521    wheretologin_label = Label(login,width=20,text="PlanetLab account type:")
1522
1523
1524    username_field = Entry(login)
1525    password_field = Entry(login, show="*")
1526    storkusername_field = Entry(login)
1527    wheretologin_field = Entry(login)
1528    wheretologin_field.insert(0,"www.planet-lab.org")
1529
1530    def browse_for_key():
1531       """Opens a file selection dialog to allows user to select private key file."""
1532       
1533       filename = tkFileDialog.askopenfilename(title='Select your private key file...', initialdir=(os.path.expanduser('~/.ssh')))
1534       if not filename:
1535           return None
1536       arizonareport.send_out(3, "Selected privatekey: " + str(filename))
1537       privatekeyfile_var.set(filename)
1538
1539    privatekeyfile_var = StringVar()
1540    privatekeyfile_field = Entry(login, textvariable=privatekeyfile_var, width=22, relief=SUNKEN)
1541    #privatekeyfile_field.bind("<Return>", lambda event: self.add_action_to_list() )
1542    privatekeyfile_browse = Button(login, text="Browse...", command=browse_for_key)
1543    
1544    privatekeypassword_field = Entry(login, show="*")
1545
1546
1547    def trylogin():
1548       """Attempt to login to planetlab with the provided login info, and also verify that
1549          the chosen private key file is a valid private key (and that the password for
1550          that key is valid, if necessary).
1551       
1552          This function will also use planetlabAPI.PlanetLablogin to ensure the user is
1553          authenticated with the PLC API, as planetlabAPI calls will later need to be
1554          made to get the keys for this user's slices.
1555       """
1556          
1557       global username
1558       global password
1559       global privatekey
1560       global publickey
1561       global slicekeys
1562       global wheretologin
1563       username = username_field.get().strip()
1564       password = password_field.get()
1565       privatekeyfile = privatekeyfile_var.get()
1566       privatekeypassword = privatekeypassword_field.get()
1567       wheretologin = wheretologin_field.get()
1568       if not privatekeypassword:
1569           privatekeypassword = None
1570       
1571       if len(username) == 0:
1572          ex_label.config(text="Please enter your PlanetLab username.", fg="red")
1573          return
1574       if len(password) == 0:
1575          ex_label.config(text="Please enter your PlanetLab password.", fg="red")
1576          return
1577       if len(privatekeyfile) == 0:
1578          ex_label.config(text="Please select your private key file.", fg="red")
1579          return
1580       privatekey = arizonacrypt.PrivateKey(file=privatekeyfile, password=privatekeypassword)
1581       if not privatekey.is_valid():
1582          ex_label.config(text="Invalid or encrypted private key.\nPlease reselect your private key or enter its password.", fg="red")
1583          return
1584       if wheretologin != "www.planet-lab.org" and wheretologin != "www.planet-lab.eu":
1585          ex_label.config(text="Invalid authentication site\nPlease use www.planetlab-org or www.planet-lab.eu", fg="red")
1586          return
1587       publickey = privatekey.get_public_key() # we can do this because we now know the privatekey is valid
1588       if authenticate(username, password,wheretologin):
1589          authenticated=True
1590
1591          # user is now logged in and has a valid key
1592          
1593          # login to the PLC API so later usage of planetlabAPI with calls the require
1594          # being authenticated work (specifically, GetSliceKeys)
1595          #planetlabAPI.PlanetLablogin(username, password, authsite=wheretologin)
1596
1597          # this public key will get added to the key database in storkusername even
1598          # if this key isn't an administrator of a slice. This could result in differeng
1599          # files being loaded by the gui than would be used by stork
1600          #arizonaconfig.set_option("publickeyfile", publickey.file)
1601          #arizonaconfig.set_option("defaultplanetlabkeys", [publickey.string])
1602
1603          # we don't know the slice name yet, so make this empty
1604          arizonaconfig.set_option("username", "")
1605
1606          login.destroy()
1607       else:
1608          ex_label.config(text="Invalid username/password. Please try again", fg="red")
1609
1610    button_frame   = Frame(login)
1611    ok_button      = Button(button_frame,text="Ok", command=trylogin)
1612    cancel_button  = Button(button_frame,text="Cancel",command=lambda: sys.exit(0) )
1613    cancel_button.grid(row=0, column=0, sticky=E)
1614    ok_button.grid(row=0, column=1, sticky=E)
1615
1616    ex_label.grid(row=0, column=0, columnspan=2, sticky=NW)
1617    username_label.grid(row=1, column=0, sticky=W)
1618    username_field.grid(row=1, column=1, sticky=W)
1619    password_label.grid(row=2, column=0, sticky=W)
1620    password_field.grid(row=2, column=1, sticky=W)
1621    privatekeyfile_label.grid(row=3, column=0,sticky=W)
1622    privatekeyfile_field.grid(row=3, column=1,sticky=W)
1623    privatekeyfile_browse.grid(row=3, column=2,sticky=W)
1624    privatekeypassword_label.grid(row=4, column=0,sticky=W)
1625    privatekeypassword_field.grid(row=4, column=1,sticky=W)
1626    wheretologin_label.grid(row=5,column=0,sticky=W)
1627    wheretologin_field.grid(row=5,column=1,sticky=W)
1628    
1629    button_frame.grid(row=6, column=0, columnspan=2, sticky=E)
1630
1631    username_field.bind("<Return>", lambda event: password_field.focus())
1632    password_field.bind("<Return>", lambda event: privatekeyfile_field.focus())
1633 #   storkusername_field.bind("<Return>", lambda event: privatekeyfile_field.focus())
1634    privatekeyfile_field.bind("<Return>",lambda event: privatekeypassword_field.focus())
1635    privatekeypassword_field.bind("<Return>",lambda event: wheretologin_field.focus())
1636    wheretologin_field.bind("<Return>", lambda event: ok_button.invoke())
1637    username_field.focus()
1638
1639    login.geometry("+%d+%d" % (300, 300))
1640
1641    arizonareport.send_out(2, "Checking for GUI updates...")
1642    check_for_update()
1643
1644    login.mainloop() 
1645
1646
1647 def close_window_callback():
1648    """This function is called when the user attempts to exit out of the GUI.
1649       If the GUI isn't synced with the repo, the user will be warned.
1650    """
1651
1652    global synched
1653    if synched or askokcancel("Exit without synching?", "You have changes that are not synched with the repository. These changes will be lost if you exit. \n\nClick OK to exit without synching.", default=CANCEL):
1654       global root
1655       root.destroy()
1656       cleanUpFiles()
1657
1658
1659 def check_for_update():
1660    """Checks for a newer version of GUI-related files and, if any are found, asks
1661       the user if they want to update them. If files are updated, the GUI
1662       will close without restarting (user needs to manually restart).
1663    """
1664       
1665    global checkedforupdate
1666    if not checkedforupdate:
1667       uptodate, version, component = sc.is_latest_version(guiversion)
1668
1669       if not uptodate and version != "unknown":
1670          showstring = "The "+str(component)+" module is currently at version \n"
1671          if component == "storkslicemanager":
1672             showstring = showstring + guiversion
1673          elif component == "storkcurlfuncs": 
1674             showstring = showstring + sc.scversion
1675
1676          showstring = showstring + "\nand can be autoupdated to\n"+version+"\n"
1677          showstring = showstring + "Would you like to update?\n"
1678
1679          if askyesno("Out of date", showstring):
1680             sc.update_gui(scriptpath)
1681
1682       checkedforupdate = True
1683
1684
1685 def addOptionToConfigFileIfMissing(option, defaultvalue=None, comments=None):
1686     """Adds option to ~/.storkmanager.conf if it is missing from that file.
1687        If defaultvalue is supplied, the option will be added as:
1688            option = defaultvalue
1689        otherwise, a non-valued option will be added as:
1690            option
1691         
1692        Returns True if option was added, False if it already existed.
1693     """
1694     
1695     global config_fn
1696     
1697     configfile = file(config_fn, "r")
1698     optionexists = False
1699     for line in configfile:
1700         if line.find("#") != -1:
1701             line = line[:line.find("#")]
1702         line = line.strip()
1703         if line.find("=") == -1:
1704             currentoption = line
1705         else:
1706             configpair = sc.read_config_line(line)
1707             if configpair != None:
1708                 currentoption = configpair[0]
1709             else:
1710                 currentoption = ""
1711         # if currentoption == option, the option is already in the file so we're done
1712         if currentoption == option:
1713             configfile.close()
1714             return False
1715
1716     # if we get to this point, then the option isn't in the file
1717     # append the option to the end of the file
1718     configfile.close()
1719     configfile = file(config_fn, "a")
1720     configfile.write("\n")
1721     if comments:
1722         for comment in comments:
1723             configfile.write("# " + comment + "\n")
1724     if defaultvalue != None:
1725         configfile.write(option + " = " + defaultvalue)
1726     else:
1727         configfile.write(option)
1728     
1729     return True
1730
1731
1732 def main():
1733    # if the config file doesn't exist, create it
1734    global config_fn
1735    if not os.path.isfile(config_fn):
1736        configfile = file(config_fn, "w")
1737        configfile.write("# Set the verbosity.\n")
1738        configfile.write("# Options are: veryquiet, quiet, verbose, veryverbose, or ultraverbose\n")
1739        configfile.write("quiet\n")
1740        configfile.close()
1741        
1742 #      try:
1743 #         f = open( os.path.expanduser("~/.storkmanager.conf"), "w")
1744 #         #TODO probably add other settings
1745 #         f.write("localpackageinfo = /tmp/packageinfo\n")
1746 #         f.write("tarpackinfo = /tmp/tarinfo\n")
1747 #         f.write("keydir = /tmp/slicekeys\n")
1748 #         f.write("metafilecachetime = 0\n\n")
1749 #         f.write("# use the PLC API rather than the nodemanager to get slice keys.\n")
1750 #         f.write("plckeysmethod = planetlabAPI\n")
1751 #         f.write("# Set the verbosity.\n")
1752 #         f.write("# Options are: veryquiet, quiet, verbose, veryverbose, or ultraverbose\n")
1753 #         f.write("veryverbose\n\n")
1754 #         f.write("# Configure the repository to use. See stork --help for more info.\n")
1755 #         f.write("repositoryhost = stork-repository.cs.arizona.edu\n")
1756 #         f.write("repositorypath = https://stork-repository.cs.arizona.edu/user-upload/\n")
1757 #         f.write("repositorypackageinfo = stork-repository.cs.arizona.edu/packageinfo\n\n")
1758 #         f.write("# Methods to use, in order of preference, to transfer files.\n")
1759 #         f.write("transfermethod = coblitz,coral,http,ftp\n")
1760 #         f.close()
1761 #      except:
1762 #         pass
1763
1764    # for any required options that don't exist in ~/.storkmanager.conf, add them
1765    # to the file using the default values the gui should use
1766    options = []
1767    options.append(("localpackageinfo", "/tmp/packageinfo", ["Directory in which to store temporary files of package information."]))
1768    options.append(("tarpackinfo", "/tmp/tarinfo", ["Directory in which to store temporary archive files."]))
1769    options.append(("keydir", "/tmp/slicekeys", ["Directory in which to store temporary key files."]))
1770    options.append(("metafilecachetime", "0", ["How long to cache the metafile for. 0 is never."]))
1771    options.append(("plckeysmethod", "planetlabAPI", ["How to access slice keys."]))
1772    # too annoying to try to make this type of option work, so just making it written to the default file
1773    # and that's it. maybe a future world wouldn't have config options that don't have values and something
1774    # like ConfigParser could be used for much of this
1775    #options.append((["veryquiet","quiet","verbose","veryverbose","ultraverbose"], None, 
1776    #                 ["Set the verbosity.", "Options are: veryquiet, quiet, verbose, veryverbose, or ultraverbose"]))
1777    options.append(("repositoryhost", "stork-repository.cs.arizona.edu", ["Configure the repository to use. See stork --help for more info."]))
1778    #options.append(("repositorypath", "https://stork-repository.cs.arizona.edu/user-upload/", None))
1779    options.append(("repositorypackageinfo", "stork-repository.cs.arizona.edu/packageinfo", None))
1780    options.append(("transfermethod", "coblitz,coral,http,ftp", ["Methods to use, in order of preference, to download files from the repository."]))
1781
1782    for (option, defaultvalue, comments) in options:
1783        addOptionToConfigFileIfMissing(option, defaultvalue=defaultvalue, comments=comments)
1784
1785    # load the options, using the cobfug file
1786    arizonaconfig.init_options('storkslicemanager.py', usage="", configfile_optvar='managerconf', version='2.0')
1787
1788    # if we can't find curl and we expect to use it, print a message and exit
1789    if not os.path.isfile(arizonaconfig.get_option("curlpath")) and 'HOME' in os.environ:
1790        arizonareport.send_error(0, "Could not find required curl executable at " + arizonaconfig.get_option("curlpath")
1791                                 + "\nTo fix this, supply the proper --curlpath setting or set curlpath in "
1792                                 + arizonaconfig.get_option("managerconf"))
1793        sys.exit(1)
1794    
1795    global repository
1796    repository = arizonaconfig.get_option("repositoryhost")
1797    sc.repository = "https://" + repository
1798
1799    if not debug:
1800       makeloginwindow()
1801      
1802    global switch_user
1803    switch_user = False
1804
1805    global root
1806    root = Tk()
1807    root.title('Stork Slice Manager')
1808    root.width=800
1809    root.height=600
1810    root.grid_propagate(True)
1811
1812    #create main scrollable canvas
1813    cnv = Canvas(root, width=root.width-20, height=600)
1814    cnv.grid(row=0, column=0, sticky='nswe')
1815    vScroll = Scrollbar(root, orient=VERTICAL, command=cnv.yview)
1816    vScroll.grid(row=0, column=1, sticky='ns')
1817    cnv.configure(yscrollcommand=vScroll.set)
1818    #make a frame to put in the canvas
1819    global frm
1820    frm = Frame(cnv)
1821    #put the frame in the canvas's scrollable area
1822    cnv.create_window(0,0, window=frm, anchor='nw')
1823
1824    global statusframe
1825    statusframe= StatusFrame(frm)
1826
1827    global topblock
1828    topblock   = TopBlock(frm)
1829
1830    global topoptions
1831    topoptions = TopOptions(frm,root)
1832
1833    #groupframes.append(group)
1834    #group.config(bd=2, relief=GROOVE)
1835    
1836    # place it somewhere on the screen that makes sense
1837    #frm.geometry("%dx%d" % (300, 500) )
1838    root.geometry("%dx%d+%d+%d" % (800, 600, 50, 50))
1839    
1840    # setup some event handlers
1841
1842    def somethinghappened(event):
1843       frm.update_idletasks()
1844       cnv.configure(scrollregion=(0, 0, frm.winfo_width(), frm.winfo_height()))
1845
1846    def scrollDown(event):
1847       cnv.yview_scroll(2, 'units')
1848       
1849    def scrollUp(event):
1850       cnv.yview_scroll(-2, 'units')
1851
1852    root.bind('<Button-4>', scrollUp)
1853    root.bind('<Button-5>', scrollDown)
1854
1855    frm.bind("<Configure>", somethinghappened)
1856
1857    frm.grid_propagate(True)
1858    frm.update_idletasks()
1859    cnv.configure(scrollregion=(0, 0, 800, frm.winfo_height()))
1860
1861    
1862    topoptions.grid(pady=10,row=0, column=0, sticky=NW)
1863    statusframe.grid(pady=10,row=0,column=1, sticky=NW)
1864   
1865 #   group = Group(frm, "All", topblock)
1866 #   group.grid(pady=3, columnspan=2, sticky=NW)
1867 #   groupframes["All"] = group
1868 #
1869 #   for foo in groups:
1870 #      if foo == "All": continue
1871 #      group = Group(frm, foo, topblock)
1872 #      group.config(relief=GROOVE)
1873 #      group.grid(pady=3,columnspan=2, sticky=NW) 
1874 #      groupframes[foo] = group
1875 #
1876 #   groups.append("All")
1877 #
1878 #   if len(groups) > 0:
1879 #      topblock.set_group( groups[-1] )  
1880  
1881    topblock.grid(pady=20, sticky=NW)
1882    
1883    # assign a callback function for when the user closes the window        
1884    root.protocol("WM_DELETE_WINDOW", close_window_callback)
1885
1886    # setup the menu bar
1887    menubar = Menu(root)
1888    # File menu
1889    filemenu = Menu(menubar, tearoff=0)
1890    filemenu.add_command(label="Exit", command=close_window_callback)
1891    menubar.add_cascade(label="File", menu=filemenu)
1892    # User menu
1893 #   usermenu = Menu(menubar, tearoff=0)
1894 #   usermenu.add_command(label="Switch user", command=topoptions.switchuser)
1895 #   menubar.add_cascade(label="User", menu=usermenu)
1896    # Repository menu
1897    repositorymenu = Menu(menubar, tearoff=0)
1898    repositorymenu.add_command(label="Synch with repository", command=lambda: statusframe.set_in_synch(True))
1899    menubar.add_cascade(label="Repository", menu=repositorymenu)
1900    # Help menu
1901    helpmenu = Menu(menubar, tearoff=0)
1902    helpmenu.add_command(label="Stork website", command=lambda: webbrowser.open("http://www.cs.arizona.edu/stork/"))
1903    helpmenu.add_command(label="Stork forum", command=lambda: webbrowser.open("http://cgi.cs.arizona.edu/projects/stork/forum/"))
1904    menubar.add_cascade(label="Help", menu=helpmenu)
1905    # display the menu
1906    root.config(menu=menubar)
1907
1908    root.mainloop()
1909
1910
1911 if __name__ == "__main__":
1912
1913    while True:  
1914       main()
1915       arizonareport.send_out(2, "Exiting.")
1916       if not switch_user:
1917          break