import repository from arizona
[raven.git] / apps / ravenpublish / ravengui.py
1 #! /usr/bin/env python
2
3 import functools
4 import os
5 import pickle
6 import platform
7 import select
8 import shutil
9 import signal
10 import sys
11 import threading
12 import time
13 import traceback
14 import urllib2
15 import urlparse
16 from PyQt4.QtCore import *
17 from PyQt4.QtGui import *
18
19 from errorparser import ErrorParser
20 import slicerun
21 import container
22 import raven
23 import ravenlib.crypto
24 import ravenlib.files.tpparse
25 import ravenlib.package.storkpackage
26
27 import ravengui_passphrase
28
29 def Error(parent, text):
30     QMessageBox.warning(parent, "Error", text)
31
32 def Warning(parent, text):
33     QMessageBox.warning(parent, "Warning", text)
34
35 def Confirm(parent, text):
36     box = QMessageBox(parent)
37     box.setText(text)
38     box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
39     ret = box.exec_()
40     return (ret == QMessageBox.Yes)
41
42 def QStringListToStringList(x):
43     l = list(x)
44     l2 = []
45     for item in l:
46         l2.append(str(item))
47     return l2
48
49 class FileCopierThread(QThread):
50     def __init__(self, parent):
51         super(FileCopierThread, self).__init__(parent)
52         self.fileList = []
53
54     def copyToName(self, srcName, destName):
55         entry = {"srcName": srcName, "destName": destName}
56         self.fileList.append(entry)
57         return entry
58
59     def copyToDir(self, srcName, destDir):
60         destName = os.path.join(destDir, os.path.basename(srcName))
61         return self.copyToName(srcName, destName)
62
63     def run(self):
64         successList = []
65         failList = []
66         for entry in self.fileList:
67             srcName = entry["srcName"]
68             destName = entry["destName"]
69             try:
70                 shutil.copyfile(srcName, destName)
71                 successList.append(entry)
72                 success = True
73             except:
74                 failList.append(entry)
75                 success = False
76                 raise
77
78             if success:
79                 self.emit(SIGNAL("copied(PyQt_PyObject)"), entry)
80             else:
81                 self.emit(SIGNAL("failed(PyQt_PyObject)"), entry)
82
83         result = {"successList": successList, "failList": failList, "fileList": self.fileList}
84         self.emit(SIGNAL("complete(PyQt_PyObject)"), result)
85
86 class TarballDownloaderThread(QThread):
87     def __init__(self, url, destdir, parent):
88         super(TarballDownloaderThread, self).__init__(parent)
89         self.url = url
90         self.destdir = destdir
91
92     def run(self):
93         aurl = urllib2.urlopen(self.url)
94
95         filename = os.path.basename(urlparse.urlsplit(self.url).path)
96         pathname = os.path.join(self.destdir, filename)
97
98         outfile = open(pathname, "w")
99         while True:
100             data = aurl.read(4096)
101             if not data:
102                break
103             outfile.write(data)
104
105         outfile.close()
106         self.emit(SIGNAL("downloaded()"))
107
108         destdir = os.path.join(self.destdir, filename.replace(".tar.bz2", ""))
109
110         if not os.path.exists(destdir):
111             os.makedirs(destdir)
112
113         # unpack it
114         os.system("tar -xjf " + pathname + " -C " + destdir)
115
116         self.emit(SIGNAL("unpacked()"))
117
118 class TpfileReaderThread(QThread):
119     def __init__(self, parent):
120         super(TpfileReaderThread, self).__init__(parent)
121
122         self.container = None
123
124         self.mutex = QMutex()
125         self.semaphoreRun = QSemaphore()
126         self.stopSignal = False
127
128         self.start()
129
130     def run(self):
131         while True:
132             # wait for a signal and get the container
133             self.semaphoreRun.acquire()
134             self.mutex.lock()
135             container = self.container
136             self.mutex.unlock()
137
138             if self.stopSignal:
139                 return
140
141             self.emit(SIGNAL("starting()"))
142
143             raventool = raven.Raven()
144             raventool.setStdoutStream(open("/dev/null", "wt"))    # silent output
145             tpfiles = raventool.build_tpfiles(container, save=False)
146             self.emit(SIGNAL("newPackageData(PyQt_PyObject)"), tpfiles)
147
148     def stop(self):
149         # set stopSignal to tell the thread to quit, and signal the semaphore
150         # so it wakes up
151         self.stopSignal=True
152         self.semaphoreRun.release()
153         # now, wait for the thread to exit
154         self.wait()
155
156     def setContainer(self, container):
157         self.mutex.lock()
158         self.container = container
159         self.mutex.unlock()
160
161     def runNow(self):
162         self.semaphoreRun.release()
163
164 class TpfileParserThread(QThread):
165     def __init__(self, filename, parent):
166         super(TpfileParserThread, self).__init__(parent)
167
168         self.filename = filename
169
170     def run(self):
171         self.parser = ravenlib.files.tpparse.tpparse(fn=self.filename)
172
173         self.emit(SIGNAL("parsed(PyQt_PyObject)"), self.parser)
174
175 class DummyWriter():
176     def __init__(self, func):
177         self.writeFunc = func
178         self.messageBuf = ""
179
180     def write(self, x):
181         self.messageBuf = self.messageBuf + x
182         while ("\n" in self.messageBuf):
183             (line, self.messageBuf) = self.messageBuf.split("\n",1)
184             self.writeLine(line)
185
186     def writeLine(self, x):
187         self.writeFunc(x)
188
189     def flush(self): pass
190     def close(self): pass
191
192 class FuncRunnerThread(QThread):
193     def __init__(self, func, parent):
194         super(FuncRunnerThread, self).__init__(parent)
195
196         self.func = func
197         self.messageBuf = ""
198
199     def run(self):
200         self.result = self.func()
201         self.emit(SIGNAL("finished(PyQt_PyObject)"), self)
202
203     def createStream(self):
204         return DummyWriter(self.writeLine)
205
206     def writeLine(self, line):
207         self.emit(SIGNAL("print(QString)"), QString(line))
208
209 class OverlayWidget(QWidget):
210     def __init__(self, parent):
211         super(OverlayWidget, self).__init__(parent)
212         self.font = QFont()
213         self.font.setBold(True)
214         self.font.setPixelSize(48)
215         self.text=""
216
217     def paintEvent(self, event):
218         if self.text:
219             painter = QPainter(self)
220             painter.setFont(self.font)
221             painter.setOpacity(0.2)
222             painter.drawText(self.rect(), Qt.AlignCenter, self.text)
223
224     def setText(self, text):
225         self.text = text
226         # even if we don't paint, if the widget isn't hidden, then it'll
227         # intercept the mouse.
228         if text:
229             self.show()
230         else:
231             self.hide()
232         self.update()
233
234 class TableWithOverlay(QTableWidget):
235     def __init__(self):
236         super(TableWithOverlay, self).__init__()
237         self.overlay = OverlayWidget(self)
238
239     def resizeEvent(self, event):
240         self.overlay.resize(event.size())
241
242     def setOverlayText(self, text):
243         self.overlay.setText(text)
244
245 class RavenWidget(QWidget):
246     def __init__(self, parent=None):
247         super(RavenWidget, self).__init__(parent)
248         self.showingBody = False
249         self.fancyAddNameDialog = False
250         self.setup()
251
252         self.tpFileReader = TpfileReaderThread(self)
253         self.connect(self.tpFileReader, SIGNAL("newPackageData(PyQt_PyObject)"), self.onNewPackageData)
254         self.connect(self.tpFileReader, SIGNAL("starting()"), self.onTpfileReaderStarting)
255
256         self.initializeRaven()
257
258         self.dir = ""
259
260         # links to sface
261         self.getDefaultKeysFunc = None
262         self.getRspecFunc = None
263         self.getSliceNameFunc = None
264         self.signalAllFunc = None
265
266         self.settingsFileName = os.path.expanduser("~/.ravengui")
267
268         self.loadSettings()
269
270         # if we saved a directory from last time, open it
271         if self.dir:
272             self.openContainer()
273
274     def cleanup(self):
275         if self.tpFileReader:
276             self.tpFileReader.stop()
277             self.tpFileReader=None
278
279     def loadSettings(self):
280         if os.path.exists(self.settingsFileName):
281             parser = ErrorParser()
282             parser.Read(self.settingsFileName, True)
283             dir = parser.GetOpt("lastsession", "dir", "")
284
285             if os.path.exists(dir) and os.path.exists(os.path.join(dir,"raven.conf")):
286                 self.dir = dir
287
288     def saveSettings(self):
289         parser = ErrorParser()
290         parser.add_section("lastsession")
291         parser.set("lastsession", "dir", self.dir)
292         parser.write(open(self.settingsFileName, "w"))
293
294     def setup(self):
295         # header
296         self.labelExperimentName = QLabel("Experiment:")
297         buttonOpenExperiment = QPushButton("&Browse")
298         buttonNewExperiment = QPushButton("&New")
299         layoutHeader = QHBoxLayout()
300         layoutHeader.addWidget(self.labelExperimentName)
301         layoutHeader.addWidget(buttonOpenExperiment)
302         layoutHeader.addWidget(buttonNewExperiment)
303         layoutHeader.addStretch()
304
305         # package list
306         self.labelPackages = QLabel("Packages to install:")
307         self.tablePackages = TableWithOverlay() #QTableWidget()
308         self.tablePackages.setSelectionBehavior(QAbstractItemView.SelectRows)
309         self.tablePackages.setSelectionMode(QAbstractItemView.SingleSelection)
310         self.tablePackages.setMinimumHeight(150)
311         self.labelPackages.setBuddy(self.tablePackages)
312
313         # button bar (packages)
314         buttonAdd = QPushButton("&Add RPM")
315         buttonAddName = QPushButton("Add &Name")
316         buttonDelete = QPushButton("&Delete")
317         buttonRefresh = QPushButton("&Refresh")
318         layoutPackageButtons = QHBoxLayout()
319         layoutPackageButtons.addWidget(buttonAdd)
320         layoutPackageButtons.addWidget(buttonAddName)
321         layoutPackageButtons.addWidget(buttonDelete)
322         layoutPackageButtons.addWidget(buttonRefresh)
323         layoutPackageButtons.addStretch()
324
325         labelMessages = QLabel("Messages:")
326         self.browserMessages = QTextBrowser()
327         labelMessages.setBuddy(self.browserMessages)
328
329         # button bar (raven)
330         buttonPublish = QPushButton("&Publish")
331         buttonBuild = QPushButton("&Build")
332         buttonConfig = QPushButton("&Config")
333         buttonSliceTools = QPushButton("&Slice Tools")
334         layoutRavenButtons = QHBoxLayout()
335         layoutRavenButtons.addWidget(buttonPublish)
336         layoutRavenButtons.addWidget(buttonBuild)
337         layoutRavenButtons.addWidget(buttonConfig)
338         layoutRavenButtons.addWidget(buttonSliceTools)
339         layoutRavenButtons.addStretch()
340
341         layoutBody = QVBoxLayout()
342         layoutBody.addWidget(self.labelPackages)
343         layoutBody.addWidget(self.tablePackages)
344         layoutBody.addLayout(layoutPackageButtons)
345         layoutBody.addWidget(labelMessages)
346         layoutBody.addWidget(self.browserMessages)
347         layoutBody.addLayout(layoutRavenButtons)
348         self.layoutBody = layoutBody
349
350         layout = QVBoxLayout()
351         layout.addLayout(layoutHeader)
352         #layout.addLayout(layoutBody)
353         self.layoutMain = layout
354
355         self.setLayout(layout)
356
357         self.backgroundOps = []
358
359         self.connect(buttonNewExperiment, SIGNAL("clicked()"), self.onNewClicked)
360         self.connect(buttonOpenExperiment, SIGNAL("clicked()"), self.onOpenClicked)
361
362         self.connect(buttonAdd, SIGNAL("clicked()"), self.onAddClicked)
363         self.connect(buttonAddName, SIGNAL("clicked()"), self.onAddNameClicked)
364         self.connect(buttonDelete, SIGNAL("clicked()"), self.onDeleteClicked)
365         self.connect(buttonRefresh, SIGNAL("clicked()"), self.updatePackageData)
366
367         self.connect(buttonPublish, SIGNAL("clicked()"), self.onPublishClicked)
368         self.connect(buttonBuild, SIGNAL("clicked()"), self.onBuildClicked)
369         self.connect(buttonConfig, SIGNAL("clicked()"), self.onConfigClicked)
370         self.connect(buttonSliceTools, SIGNAL("clicked()"), self.onSliceToolsClicked)
371
372     def initializeRaven(self):
373         ravenlib.package.storkpackage.initialize()
374
375     def openContainer(self):
376         c = container.container()
377         c.set_dir(self.dir)
378         if not c.exists():
379             Error(self, "No container in directory " + self.dir)
380             sys.exit(-1)
381         c.load()
382
383         if not self.showingBody:
384             self.showingBody = True
385             self.layoutMain.addLayout(self.layoutBody)
386
387         self.container = c
388
389         self.labelExperimentName.setText("Experiment: " + c.get_name())
390
391         self.updatePackageData()
392
393         # save the new starting container directory
394         self.saveSettings()
395
396     def onNewClicked(self):
397         # get the users key/cred from SFA if it's available
398         if self.getDefaultKeysFunc:
399             (defaultKeyName, defaultCredName) = self.getDefaultKeysFunc()
400         else:
401             defaultKeyName = None
402             defaultCredName = None
403
404         if self.getSliceNameFunc:
405             defaultSliceName = self.getSliceNameFunc()
406         else:
407             defaultSliceName = None
408
409         wiz = ConfigWizard(None, sliceName=defaultSliceName, keyName=defaultKeyName, credName=defaultCredName, parent=self)
410         if (wiz.exec_() == 1):
411             c = container.container()
412             wiz.updateContainer(c)
413             c.check_update_id()
414             c.makedirs()
415             c.copy_initial_files()
416             c.save()
417             self.dir = c.get_rootdir()
418             self.openContainer()
419
420             if self.signalAllFunc:
421                 self.signalAllFunc("ravenConfigChanged")
422
423     def onConfigClicked(self):
424         wiz = ConfigWizard(self.container, self)
425         if (wiz.exec_() == 1):
426             wiz.updateContainer(self.container)
427             self.container.save()
428
429     def onSliceToolsClicked(self):
430         if not self.getRspecFunc:
431             Warning(self, "Slice tools are only usable when Raven is used from inside sface")
432             return
433         (style, sliceHrn, rspecFileName) = self.getRspecFunc()
434         if (not style) or (not sliceHrn) or (not rspecFileName):
435             Warning(self, "Sface must download a copy of your rspec before slice tools can be used")
436             return
437         dlg = DialogSliceTools(self.container, style, sliceHrn, rspecFileName, self)
438         dlg.show()
439
440     def onOpenClicked(self):
441         dirName = QFileDialog.getExistingDirectory(self, "Open Experiment", os.path.expanduser("~"))
442         dirName = str(dirName)
443
444         if (not dirName):
445             return
446
447         if not os.path.exists(os.path.join(dirName, "raven.conf")):
448             Error(self, "The directory you selected is not a raven experiment container.")
449             return
450
451         self.dir = dirName
452         self.openContainer()
453
454         if self.signalAllFunc:
455             self.signalAllFunc("ravenConfigChanged")
456
457     def onAddClicked(self):
458         fileNames = QFileDialog.getOpenFileNames(self, "Add RPM", os.path.expanduser("~"), "RPM Files (*.rpm)")
459         fileNames = QStringListToStringList(fileNames)
460
461         if fileNames:
462            t = FileCopierThread(self)
463            for fileName in fileNames:
464                t.copyToDir(fileName, self.container.get_packagedir())
465
466            self.connect(t, SIGNAL("copied(PyQt_PyObject)"), self.onFileCopied)
467            self.connect(t, SIGNAL("failed(PyQt_PyObject)"), self.onFileFailed)
468            self.connect(t, SIGNAL("complete(PyQt_PyObject)"), self.onFileCopyComplete)
469
470            self.browserMessages.append("Copying files to package dir...")
471            t.start()
472
473     def onFileCopied(self, fileDict):
474         self.browserMessages.append("  copied " + str(fileDict["srcName"]) + " to " + str(fileDict["destName"]))
475
476     def onFileFailed(self, fileDict):
477         self.browserMessages.append("  failed to copy " + str(fileDict["srcName"]) + " to " + str(fileDict["destName"]))
478
479     def onFileCopyComplete(self, resultDict):
480         self.browserMessages.append("Copy complete. Refreshing.")
481         self.updatePackageData()
482
483     def onAddNameClicked(self):
484         if self.fancyAddNameDialog:
485             dlg = DialogAddName(parent=self, container=self.container)
486             dlg.exec_()
487
488             self.updatePackageData()
489         else:
490             (s,okClicked) = QInputDialog.getText(None, "Add package by name", "Package Name:")
491             if okClicked:
492                 fn = str(s)
493                 fn = os.path.join(self.container.get_packagedir(), fn) + ".name"
494                 f = open(fn, "wt")
495                 f.close()
496                 self.updatePackageData()
497
498     def onDeleteClicked(self):
499         if (len(self.tablePackages.selectedItems()) <= 0):
500             Warning(self, "Select something in the list before pressing delete.")
501             return
502
503         data = str(self.tablePackages.selectedItems()[0].data(Qt.UserRole).toString())
504         data = pickle.loads(data)
505
506         if data["packagename"].endswith("_ravenconfig"):
507             Error(self, "The ravenconfig package should not be removed")
508             return
509
510         box = QMessageBox(self)
511         box.setText("Confirm Deletion of " + data["packagename"] + " ?")
512         box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
513         ret = box.exec_()
514         if (ret != QMessageBox.Yes):
515             return
516
517         if "srcFileName" in data:
518            print "removing:", data["srcFileName"]
519            try:
520                os.remove(data["srcFileName"])
521            except IOError:
522                Warning(self, "failed to remove file")
523                return
524
525            row = self.tablePackages.selectionModel().selectedRows()[0].row()
526            self.tablePackages.removeRow(row)
527
528     def onPublishClicked(self):
529         if self.backgroundOps:
530             Error(self, "Please wait for other operations to complete")
531             return
532         self.container.get_privateKey() # makes sure we have our passphrase set
533         raventool = raven.Raven()
534         raventool.main_parse(["publish"])
535         self.wrap(raventool, "publish", functools.partial(raventool.do_publish, self.container, raventool.cmd_opts, []))
536
537     def onBuildClicked(self):
538         if self.backgroundOps:
539             Error(self, "Please wait for other operations to complete")
540             return
541         self.container.get_privateKey() # makes sure we have our passphrase set
542         raventool = raven.Raven()
543         raventool.main_parse(["build"])
544         self.wrap(raventool, "build", functools.partial(raventool.do_build, self.container, raventool.cmd_opts, []))
545
546     def wrap(self, raventool, description, func):
547         t = FuncRunnerThread(func, self)
548
549         if raventool:
550             raventool.setStdoutStream(t.createStream())
551
552         self.connect(t, SIGNAL("finished(PyQt_PyObject)"), self.onThreadFinished)
553         self.connect(t, SIGNAL("print(QString)"), self.onMessagePrint)
554         self.backgroundOps.append( {"description": description, "thread": t} )
555
556         t.start()
557
558     def onMessagePrint(self, line):
559         self.browserMessages.append(line)
560         #print str
561
562     def onThreadFinished(self, thread):
563         for proc in self.backgroundOps:
564             if proc.get("thread", None) == thread:
565                 self.backgroundOps.remove(proc)
566                 self.browserMessages.append("Operation completed: " + str(proc["description"]))
567                 self.disconnect(thread, SIGNAL("finished(PyQt_PyObject)"), self.onThreadFinished)
568                 self.disconnect(thread, SIGNAL("print(QString)"), self.onMessagePrint)
569                 return
570         print "unknown child completed:", thread
571
572     def onChildExit(self, pid):
573         for proc in self.backgroundOps:
574             if proc.get("pid", -1) == pid:
575                 self.backgroundOps.remove(proc)
576                 self.browserMessages.append("Operation completed: " + str(proc["description"]) + " (pid " + str(pid) + ")")
577                 #print "COMPLETE:", proc
578                 return
579         print "unknown child completed:", pid
580
581     def addTableItem(self, table, row, col, val, data=None, readonly=True):
582         item = QTableWidgetItem(str(val))
583         if readonly:
584             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
585         if data:
586             if not isinstance(data, str):
587                data = pickle.dumps(data)
588             item.setData(Qt.UserRole, QVariant(data))
589         table.setItem(row, col, item)
590
591     def updatePackageData(self):
592         self.tpFileReader.setContainer(self.container)
593         self.tpFileReader.runNow()
594
595     def onTpfileReaderStarting(self):
596         self.tablePackages.setOverlayText("Refreshing...")
597
598     def onNewPackageData(self, tp):
599         self.tablePackages.clear()
600         self.tablePackages.setColumnCount(4)
601         self.tablePackages.setHorizontalHeaderLabels(["name", "kind", "action", "tags"])
602
603         self.tablePackages.setRowCount(len(tp.contents))
604
605         row = 0
606         for entry in tp.contents:
607             if entry["type"] == "user":
608                 self.addTableItem(self.tablePackages, row, 0, entry["username"] + "(" + entry["pattern"] + ")", entry)
609             else:
610                 self.addTableItem(self.tablePackages, row, 0, entry["pattern"], entry)
611
612             self.addTableItem(self.tablePackages, row, 1, entry["type"], "")
613             self.addTableItem(self.tablePackages, row, 2, entry["action"], "")
614
615             if "tags" in entry:
616                 self.addTableItem(self.tablePackages, row, 3, ",".join(entry["tags"]), "")
617
618             row = row + 1
619
620         self.tablePackages.resizeColumnsToContents()
621
622         self.tablePackages.setOverlayText("")
623
624 class DialogAddName(QDialog):
625     def __init__(self, container, parent=None):
626         super(DialogAddName, self).__init__(parent)
627         self.container = container
628         self.downloader = None
629         self.packages = None
630         self.parser = None
631         self.setup()
632         self.update(silent=True)
633
634     def setup(self):
635         self.labelPackages = QLabel("Available Packages:")
636         self.tablePackages = TableWithOverlay() #QTableWidget()
637         self.tablePackages.setSelectionBehavior(QAbstractItemView.SelectRows)
638         self.tablePackages.setSelectionMode(QAbstractItemView.SingleSelection)
639         self.tablePackages.setMinimumWidth(480) # minimum with to show the "Refreshing..." overlay
640         self.labelPackages.setBuddy(self.tablePackages)
641
642         # button bar (raven)
643         buttonRefresh = QPushButton("&Refresh")
644         layoutButtons = QHBoxLayout()
645         layoutButtons.addWidget(buttonRefresh)
646         layoutButtons.addStretch()
647
648         layout = QVBoxLayout()
649         layout.addWidget(self.labelPackages)
650         layout.addWidget(self.tablePackages)
651         layout.addLayout(layoutButtons)
652
653         self.setLayout(layout)
654
655         self.connect(buttonRefresh, SIGNAL("clicked()"), self.onRefreshClicked)
656
657     def onRefreshClicked(self):
658         if self.downloader:
659             return
660
661         self.tablePackages.setOverlayText("Downloading...")
662
663         self.downloader = TarballDownloaderThread("http://stork-repository.cs.arizona.edu/packageinfo/tpfiles-latest.tar.bz2", self.container.get_cachedir(), self)
664         self.connect(self.downloader, SIGNAL("downloaded()"), self.onDownloaderDownloaded)
665         self.connect(self.downloader, SIGNAL("unpacked()"), self.onDownloaderUnpacked)
666         self.downloader.start()
667
668     def onDownloaderDownloaded(self):
669         self.tablePackages.setOverlayText("Upacking...")
670
671     def onDownloaderUnpacked(self):
672         # this signal is being caught twice. I don't know why.
673
674         self.tablePackages.setOverlayText("")
675         # disconnect fails -- perhaps because object has already destroyed itself?
676         #self.disconnect(self.downloader, SIGNAL("downloaded()"), self.onDownloaderDownloaded)
677         #self.disconnect(self.downloader, SIGNAL("unpacked()"), self.onDownloaderUnpacked)
678         self.downloader = None
679         self.update()
680
681     def update(self, silent=False):
682         if self.parser:
683             return
684
685         dir = os.path.join(os.path.join(self.container.get_cachedir(), "tpfiles-latest"), "tpfiles")
686
687         keyString = ravenlib.crypto.keypair_to_stork_pubkey_string(self.container.get_privateKey())
688         filenames = ravenlib.files.tpparse.get_tpfile_filenames(self.container.get_name(), keyString)
689
690         filename = None
691         for candidate in filenames:
692             pathname = os.path.join(dir, candidate)
693             if os.path.exists(pathname):
694                 filename = candidate
695                 break
696
697         if not filename:
698             if not silent:
699                 Warning(self, "Unable to find your tpfile. Please publish your experiment, then use the <Refresh> button.")
700             return
701
702         self.tablePackages.setOverlayText("Refreshing...")
703         self.parser = TpfileParserThread(pathname, self)
704         self.connect(self.parser, SIGNAL("parsed(PyQt_PyObject)"), self.onParserParsed)
705         self.parser.start()
706
707     def addTableItem(self, table, row, col, val, data=None, readonly=True):
708         item = QTableWidgetItem(str(val))
709         if readonly:
710             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
711         if data:
712             if not isinstance(data, str):
713                data = pickle.dumps(data)
714             item.setData(Qt.UserRole, QVariant(data))
715         table.setItem(row, col, item)
716
717     def onParserParsed(self, parser):
718         self.tablePackages.setOverlayText("")
719
720         entries = parser.get_all_entries()
721
722         self.tablePackages.clear()
723         self.tablePackages.setColumnCount(1)
724         self.tablePackages.setHorizontalHeaderLabels(["name"])
725
726         packages = []
727         for entry in entries:
728            if entry["kind"] == "PACKAGE":
729                packages.append(entry)
730
731         self.tablePackages.setRowCount(len(packages))
732
733         row = 0
734         for entry in packages:
735             self.addTableItem(self.tablePackages, row, 0, entry["pattern"], entry)
736             row = row + 1
737
738         self.tablePackages.resizeColumnsToContents()
739
740         self.packages = packages
741         self.parser = None
742
743 class DialogSliceTools(QDialog):
744     def __init__(self, container, style, hrn, rspecFileName, parent=None):
745         super(DialogSliceTools, self).__init__(parent)
746         self.container = container
747         self.style = style
748         self.sliceHrn = hrn
749         self.rspecFileName = rspecFileName
750         self.backgroundOps = []
751         self.setup()
752
753     def setup(self):
754         labelMessages = QLabel("Status:")
755         self.browserMessages = QTextBrowser()
756         self.browserMessages.setMinimumWidth(480)
757         labelMessages.setBuddy(self.browserMessages)
758
759         # button bar (raven)
760         buttonInstall = QPushButton("&Install")
761         buttonCheck = QPushButton("&Check")
762         buttonDump = QPushButton("&Dump")
763         buttonExec = QPushButton("&Exec")
764         buttonClose = QPushButton("C&lose")
765         layoutButtons = QHBoxLayout()
766         layoutButtons.addWidget(buttonInstall)
767         layoutButtons.addWidget(buttonCheck)
768         layoutButtons.addWidget(buttonDump)
769         layoutButtons.addWidget(buttonExec)
770         layoutButtons.addWidget(buttonClose)
771         layoutButtons.addStretch()
772
773         layout = QVBoxLayout()
774         layout.addWidget(labelMessages)
775         layout.addWidget(self.browserMessages)
776         layout.addLayout(layoutButtons)
777
778         self.setLayout(layout)
779
780         self.connect(buttonInstall, SIGNAL("clicked()"), self.onInstallClicked)
781         self.connect(buttonCheck, SIGNAL("clicked()"), self.onCheckClicked)
782         self.connect(buttonDump, SIGNAL("clicked()"), self.onDumpClicked)
783         self.connect(buttonExec, SIGNAL("clicked()"), self.onExecClicked)
784         self.connect(buttonClose, SIGNAL("clicked()"), self.onCloseClicked)
785
786     def initSliceRun(self):
787         if self.backgroundOps:
788             Warning(self, "Please wait for current operation to complete.")
789             return False
790
791         self.sliceRun = slicerun.StorkInstaller(self.sliceHrn, self.style)
792         self.sliceRun.load_rspec(open(self.rspecFileName,"r").read())
793         self.sliceRun.capture_output = True
794         return True
795
796     def onInstallClicked(self):
797         if not self.initSliceRun():
798             return
799         self.wrap(self.sliceRun, "slicerun: stork install", self.sliceRun.run_and_wait)
800
801     def onDumpClicked(self):
802         if not self.initSliceRun():
803             return
804         self.wrap(self.sliceRun, "slicerun: dump", self.sliceRun.dump)
805
806     def onCheckClicked(self):
807         if not self.initSliceRun():
808             return
809         self.wrap(self.sliceRun, "slicerun: check", functools.partial(self.sliceRun.run_and_wait, script="XXX_check", show_completions=False))
810
811     def onExecClicked(self):
812         if not self.initSliceRun():
813             return
814         (s, okClicked) = QInputDialog.getText(None, "Add package by name", "Package Name:")
815         if okClicked:
816             self.wrap(self.sliceRun, "slicerun: exec", functools.partial(self.sliceRun.run_and_wait, script=str(s)))
817
818     def onCloseClicked(self):
819         self.close()
820
821     def onMessagePrint(self, line):
822         self.browserMessages.append(line)
823         #print str
824
825     def onThreadFinished(self, thread):
826         for proc in self.backgroundOps:
827             if proc.get("thread", None) == thread:
828                 self.backgroundOps.remove(proc)
829                 self.browserMessages.append("Operation completed: " + str(proc["description"]))
830                 self.disconnect(thread, SIGNAL("finished(PyQt_PyObject)"), self.onThreadFinished)
831                 self.disconnect(thread, SIGNAL("print(QString)"), self.onMessagePrint)
832                 return
833
834     def wrap(self, slicetool, description, func):
835         t = FuncRunnerThread(func, self)
836
837         if slicetool:
838             slicetool.setStdoutStream(t.createStream())
839
840         self.connect(t, SIGNAL("finished(PyQt_PyObject)"), self.onThreadFinished)
841         self.connect(t, SIGNAL("print(QString)"), self.onMessagePrint)
842         self.backgroundOps.append( {"description": description, "thread": t} )
843
844         t.start()
845
846
847 class RavenWizardPage(QWizardPage):
848     def __init__(self, parent=None):
849         super(RavenWizardPage, self).__init__(parent)
850
851     def onBrowseFileClicked(self):
852         fileName = str(self.editFileName.text())
853         if (fileName) and (fileName != "None"):
854             dirName = os.path.dirname(fileName)
855         else:
856             dirName = os.path.abspath(".")
857
858         fileName = QFileDialog.getOpenFileName(self, "Select Filename", dirName)
859         if (fileName):
860             self.editFileName.setText(fileName)
861
862     def onBrowseDirClicked(self):
863         dirName = str(self.editDirectory.text())
864         if (dirName) and (dirName != "None"):
865             pass
866         else:
867             dirName = os.path.abspath(".")
868         dirName = QFileDialog.getExistingDirectory(self, "Select Directory", dirName)
869         if (dirName):
870             self.editDirectory.setText(dirName)
871
872
873 class ConfigWizard(QWizard):
874     def __init__(self, aContainer=None, sliceName=None, keyName=None, credName=None, parent=None):
875         super(ConfigWizard, self).__init__(parent)
876         self.setWindowTitle("Configure Raven")
877         if aContainer:
878             self.create = False
879             self.container = aContainer
880         else:
881             self.create = True
882             self.container = container.container()
883
884             # when using sface, we can make use of the defailt key/cred
885             if (sliceName):
886                 self.container.set_slices([sliceName])
887             if (keyName) and (os.path.exists(keyName)):
888                 self.container.set_privateKeyName(keyName)
889             if (credName) and (os.path.exists(credName)):
890                 self.container.set_credName(credName)
891         self.setup()
892
893     def wizardPage(self, title=None, desc=None, fieldList=[]):
894         page = RavenWizardPage(self)
895         layout = QVBoxLayout()
896         if title:
897             page.setTitle(title)
898         if desc:
899             labelDesc = QLabel(desc)
900             labelDesc.setWordWrap(True)
901             layout.addWidget(labelDesc)
902         for (controlName, caption, control, defaultValue) in fieldList:
903             if (control==QCheckBox):
904                # checkboxes have the label as part of the control
905                control = control(caption, self)
906             else:
907                labelCaption = QLabel(caption)
908                layout.addWidget(labelCaption)
909                control = control(self)
910             if (controlName.endswith("FileName")) or (controlName.endswith("Directory")):
911                 buttonBrowse = QPushButton("Browse")
912                 layoutBrowse = QHBoxLayout()
913                 layoutBrowse.addWidget(control)
914                 layoutBrowse.addWidget(buttonBrowse)
915                 layout.addLayout(layoutBrowse)
916                 if (controlName.endswith("FileName")):
917                     page.connect(buttonBrowse, SIGNAL("clicked()"), page.onBrowseFileClicked)
918                     page.editFileName = control
919                 else:
920                     page.connect(buttonBrowse, SIGNAL("clicked()"), page.onBrowseDirClicked)
921                     page.editDirectory = control
922             else:
923                 layout.addWidget(control)
924             setattr(self, controlName, control)
925             setattr(page, controlName, control)
926             if defaultValue:
927                 if isinstance(control, QLineEdit):
928                     control.setText(defaultValue)
929                 elif isinstance(control, QCheckBox):
930                     control.setChecked(defaultValue)
931         page.setLayout(layout)
932         return page
933
934     def validateDirectory(self):
935         dir = os.path.expanduser(str(self.editDirectory.text()))
936         if not os.path.exists(dir):
937             if not Confirm(self, "Directory " + dir + " does not exist. Create ?"):
938                 return False
939             try:
940                 os.makedirs(dir)
941             except (OSError, IOError):
942                 Error(self, "Failed to create directory")
943                 return False
944
945         if not os.path.isdir(dir):
946             Error(self, "Object specified is not a directory")
947             return False
948
949         if os.path.exists(os.path.join(dir, "raven.conf")):
950             Error(self, "There is already a raven experiment container in that directory")
951             return False
952
953         nonEmpty=False
954         for fn in os.listdir(dir):
955            if not fn.startswith("."):
956                nonEmpty=True
957
958         if nonEmpty:
959             Error(self, "The directory is not empty. Please choose an empty directory.")
960             return False
961
962         return True
963
964     def validateExperimentName(self):
965         name = str(self.editExperimentName.text())
966         if (not name):
967             Error(self, "Experiment name cannot be blank")
968             return False
969         if (" " in name) or ("." in name):
970             Error(self, "Experiment name cannot contain whitespace or '.'")
971             return False
972         return True
973
974     def validatePrivateKey(self):
975         name = os.path.expanduser(str(self.editPrivateKeyFileName.text()))
976         if (not name) or (name=="None"):
977             Error(self, "Private key name cannot be blank")
978             return False
979         if (not os.path.exists(name)):
980             Error(self, "Private key file does not exist")
981             return False
982         if (not raven.Raven().test_privatekey(name)):
983             Error(self, "Failed to read private key")
984             return False
985         return True
986
987     def validateCred(self):
988         name = os.path.expanduser(str(self.editCredFileName.text()))
989         if (name==None) or (name=="None") or (name==""):
990             # blank is ok
991             return True
992         if (not os.path.exists(name)):
993             Error(self, "Credential file does not exist")
994             return False
995         if (not raven.Raven().test_cred(name)):
996             Error(self, "Failed to read credential")
997             return False
998
999         keyName = os.path.expanduser(str(self.editPrivateKeyFileName.text()))
1000         if (not raven.Raven().test_privatekey_cred(keyName, name)):
1001             Error(self, "The private key you specified doesn't match the public key in the credential.")
1002             return False
1003
1004         return True
1005
1006     def setup(self):
1007         if self.create:
1008             self.directoryPage = self.wizardPage("Directory",
1009                                                  "Select an existing directory for the experiment container to be created."
1010                                                  " Configuration files will be created, and subdirectories created within the directory you select.",
1011                                                  [ ("editDirectory", "Directory:", QLineEdit, os.path.abspath(".")), ])
1012             self.directoryPage.validatePage = self.validateDirectory
1013             self.addPage(self.directoryPage)
1014
1015         self.experimentNamePage = self.wizardPage("Experiment Name",
1016                                                   "The experiment name is used to keep your raven experiment separate from other raven experiments."
1017                                                   " Choose a name that has no whitespace.",
1018                                                   [ ("editExperimentName", "Experiment Name:", QLineEdit, self.container.get_name()), ])
1019         self.experimentNamePage.validatePage = self.validateExperimentName
1020         self.addPage(self.experimentNamePage)
1021
1022         self.privateKeyPage = self.wizardPage("Private Key",
1023                                               "The private key is used to sign files when uploading to the raven repository."
1024                                               " PlanetLab users - make sure to use the same key as your PlanetLab login.",
1025                                               [ ("editPrivateKeyFileName", "Private Key Filename:", QLineEdit, self.container.get_privateKeyName()), ])
1026         self.privateKeyPage.validatePage = self.validatePrivateKey
1027         self.addPage(self.privateKeyPage)
1028
1029         self.credPage = self.wizardPage("Credential",
1030                                         "A GENI Credential file may be used to automatically upload files to the Raven"
1031                                         " repository. This file is optional, but without it you will be responsible"
1032                                         " for manually uploading the files.",
1033                                         [ ("editCredFileName", "Credential Filename:", QLineEdit, self.container.get_credName()), ])
1034         self.credPage.validatePage = self.validateCred
1035         self.addPage(self.credPage)
1036
1037         self.slicePage = self.wizardPage("Slice(s)",
1038                                          "Raven may be configured to manage the config files on your slices for you."
1039                                          " You may enter multiple slice names separated by commas. Enterning no"
1040                                          " slice names will cause packages and tpfiles to be uploaded, but not"
1041                                          " slice configuration files.",
1042                                          [ ("editSlices", "Slice name(s):", QLineEdit, ",".join(self.container.get_slices())), ])
1043         self.addPage(self.slicePage)
1044
1045         self.pacmanPage = self.wizardPage("Automatic Package Management",
1046                                           "The packages.pacman file controls which packages will be installed"
1047                                           " on your nodes. This tool can be configured to automatically manage"
1048                                           " this file, by installing all of your packages on all of your nodes.",
1049                                           [ ("checkBoxManagePackages", "Manage Packages", QCheckBox, self.container.manage_packages), ])
1050         self.addPage(self.pacmanPage)
1051
1052         self.packagesPage = self.wizardPage("Automatic Package Selection",
1053                                           "The following services will be automatically maintained and updated by Raven on your slices."
1054                                           "<br><br>"
1055                                           "These options are only used if you enabled package management on the prior screen",
1056                                           [ ("checkBoxManageStork", "Stork Package Management Service", QCheckBox, self.container.upgrade_stork),
1057                                             ("checkBoxManageOwl", "Owl Monitoring Service", QCheckBox, self.container.upgrade_owld),
1058                                             ("checkBoxManageKong", "Kong Experiment Control Service", QCheckBox, self.container.upgrade_kong), ])
1059         self.addPage(self.packagesPage)
1060
1061     def updateContainer(self, c):
1062         if self.create:
1063            c.set_dir(os.path.expanduser(os.path.abspath(str(self.editDirectory.text()))))
1064
1065         c.set_name(str(self.editExperimentName.text()))
1066         c.set_privateKeyName(os.path.expanduser(str(self.editPrivateKeyFileName.text())))
1067
1068         cred = os.path.expanduser(str(self.editCredFileName.text()))
1069         if (cred == ""):
1070             cred = "None"
1071         c.set_credName(cred)
1072
1073         slices = str(self.editSlices.text())
1074         slices = [slice.strip() for slice in slices.split(",")]
1075         c.set_slices(slices)
1076
1077         c.manage_packages = self.checkBoxManagePackages.isChecked()
1078         c.update_stork = self.checkBoxManageStork.isChecked()
1079         c.update_owl = self.checkBoxManageOwl.isChecked()
1080         c.update_kong = self.checkBoxManageKong.isChecked()
1081
1082 class RavenMainWindow(QMainWindow):
1083     def __init__(self, gacks=None, parent=None):
1084         super(RavenMainWindow, self).__init__(parent)
1085         self.widget = RavenWidget(parent)
1086
1087         # quick hack for testing the new dialog
1088         if "--fancyaddnamedialog" in [x.lower() for x in sys.argv]:
1089            self.widget.fancyAddNameDialog = True
1090
1091         self.setMinimumWidth(800)
1092
1093         self.setCentralWidget(self.widget)
1094
1095         #set_passphrase_callback(ask_passphrase)
1096
1097         ravengui_passphrase.setup(self)
1098
1099     def event(self, event):
1100         if (event!=None) and (event.type() == ravengui_passphrase.PassPhraseEventType):
1101             #print "receive event"
1102             if event.wasIncorrect:
1103                 QMessageBox.warning(None, "Warning", "Passphrase incorrect. Please try again.")
1104             (s,okClicked) = QInputDialog.getText(self, "Passphrase Required", "Passphrase:")
1105             if okClicked:\r
1106                 event.set_passphrase(str(s))\r
1107             event.signal()\r
1108             return True
1109         else:
1110             return QWidget.event(self, event)
1111
1112     def closeEvent(self, event):
1113         self.widget.cleanup()
1114         super(RavenMainWindow, self).closeEvent(event)
1115
1116 def main():
1117     app = QApplication(sys.argv)
1118     form = RavenMainWindow()
1119     form.show()
1120     app.exec_()
1121
1122 if __name__=="__main__":
1123     main()
1124