Merge branch 'master' of ssh://git.onelab.eu/git/sface
authorThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Thu, 8 Sep 2011 12:25:19 +0000 (14:25 +0200)
committerThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Thu, 8 Sep 2011 12:25:19 +0000 (14:25 +0200)
sface.spec
sface/screens/configscreen.py
sface/screens/mainscreen.py
sface/screens/userscreen.py
sface/sficreate.py [new file with mode: 0644]
sface/sfiprocess.py
sface/sfirenew.py
sface/xmlrpcwindow.py

index b5914cf..257b302 100644 (file)
@@ -7,7 +7,7 @@
 
 %define name sface
 %define version 0.1
-%define taglevel 17
+%define taglevel 18
 
 %define release %{taglevel}%{?pldistro:.%{pldistro}}%{?date:.%{date}}
 
@@ -60,6 +60,10 @@ rm -rf $RPM_BUILD_ROOT
 %{_datadir}/sface/*
 
 %changelog
+* Sun Sep 04 2011 Thierry Parmentelat <thierry.parmentelat@sophia.inria.fr> - sface-0.1-18
+- usable with a different directory than ~/.sfi
+- either on the command line, or in the config. screen
+
 * Thu Sep 01 2011 Thierry Parmentelat <thierry.parmentelat@sophia.inria.fr> - sface-0.1-17
 - new user management screen to allow users to be added and deleted from slices
 - known issue with the users screen regarding adding people who are listed as PIs to a sliver.
index dc64620..37db423 100644 (file)
@@ -8,6 +8,8 @@ from sface.screens.sfascreen import SfaScreen
 from sfa.util.version import version_core
 from sface.version import version_dict
 
+from sface.sficreate import CreateWindow
+
 static_labels = {
     'slice' :  [ 
         "Sface : %s (%s)" % (version_dict()['code_tag'], version_dict()['code_url']),
@@ -45,7 +47,7 @@ class ConfigWidget(QWidget):
         glayout = QGridLayout()
         row = 0
         for (field,msg) in config.field_labels():
-            
+
             if static_labels.has_key(field):
                 labels=static_labels[field]
                 if not isinstance(labels,list): labels = [ labels, ]
@@ -90,6 +92,7 @@ class ConfigWidget(QWidget):
         hlayout.addWidget (edit)
         hlayout.addSpacing(10)
 
+        conf_button ('createSlice', 'Create New Slice'),
         conf_button ('apply','Apply Only'),
         conf_button ('save','Apply && Save')
 
@@ -100,6 +103,12 @@ class ConfigWidget(QWidget):
         self.setLayout(layout)
         self.inited=True
 
+    def createSlice(self):
+        dlg = CreateWindow(parent=self)
+        dlg.exec_()
+        if (dlg.sliceWasCreated):
+            self.slice.setText(dlg.getHrn())
+            self.save()
 
     def apply(self):
         print 'applying'
@@ -127,7 +136,7 @@ class ConfigWidget(QWidget):
     # switch to another config dir
     def load(self):
         # obtain new dor somehow
-        
+
         edit=self.retrieve_local('config_dirname')
         newdir=str(edit.text())
         newdir+='/'
index e6e4fb4..5578fbc 100644 (file)
@@ -7,7 +7,7 @@ from PyQt4.QtGui import *
 #from sfa.util.rspecHelper import RSpec
 from sfa.rspecs.rspec_parser import parse_rspec
 from sface.config import config
-from sface.sfirenew import SfiRenewer
+from sface.sfirenew import RenewWindow
 from sface.sfiprocess import SfiProcess
 from sface.screens.sfascreen import SfaScreen
 
@@ -127,7 +127,7 @@ class NodeView(QTreeView):
                         tagstring = QString("%s: %s" % (tagname, value))
                         tagItem = QStandardItem(tagstring)
                         status = QStandardItem(QString(tag_status['add']))
-                        nodeItem.appendRow([tagItem, status])
+                        nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
 
             elif status_data in (node_status['out'], node_status['remove']):
                 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
@@ -461,19 +461,7 @@ class SliceWidget(QWidget):
 
     def renew(self):
         dlg = RenewWindow(parent=self)
-        if (dlg.exec_() == QDialog.Accepted):
-            self.setStatus("Renewing Slice.")
-
-            self.renewProcess = SfiRenewer(config.getSlice(), dlg.get_new_expiration(), self)
-            self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
-
-    def renewFinished(self):
-        if self.renewProcess.statusMsg:
-            self.setStatus("Renew " + self.renewProcess.status + ": " + self.renewProcess.statusMsg)
-        else:
-            self.setStatus("Renew " + self.renewProcess.status)
-        self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
-        self.renewProcess = None
+        dlg.exec_()
 
     def refresh(self):
         if not config.getSlice():
@@ -571,44 +559,6 @@ class SliceWidget(QWidget):
     def nodeSelectionChanged(self, hostname):
         self.parent().nodeSelectionChanged(hostname)
 
-class RenewWindow(QDialog):
-    def __init__(self, parent=None):
-        super(RenewWindow, self).__init__(parent)
-        self.setWindowTitle("Renew Slivers")
-
-        self.duration = QComboBox()
-
-        self.expirations = []
-
-        durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
-
-        now = datetime.datetime.utcnow()
-        for (weeks, desc) in durations:
-            exp = now + datetime.timedelta(days = weeks * 7)
-            desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
-            self.expirations.append(exp)
-            self.duration.addItem(desc)
-
-        self.duration.setCurrentIndex(0)
-
-        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
-        buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
-
-        layout = QVBoxLayout()
-        layout.addWidget(self.duration)
-        layout.addWidget(buttonBox)
-        self.setLayout(layout)
-
-        self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
-        self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
-
-    def accept(self):
-        QDialog.accept(self)
-
-    def get_new_expiration(self):
-        index = self.duration.currentIndex()
-        return self.expirations[index]
-
 class MainScreen(SfaScreen):
     def __init__(self, parent):
         SfaScreen.__init__(self, parent)
index bcb87f0..9adf116 100644 (file)
@@ -275,7 +275,9 @@ class UsersWidget(QWidget):
             childStatus = str(item.child(row, MEMBERSHIP_STATUS_COLUMN).data(Qt.DisplayRole).toString())
 
             if (childStatus == user_status['add']):
-                slicerec.get_field("researcher").append(childName)
+                researcher = slicerec.get_field("researcher", [])
+                researcher.append(childName)
+                slicerec["researcher"] = researcher
                 change = True
             elif (childStatus == user_status['remove']):
                 if childName in slicerec.get_field("PI"):
@@ -284,7 +286,6 @@ class UsersWidget(QWidget):
                      slicerec.get_field("researcher").remove(childName)
                 change = True
 
-        print "XXX", slicerec.get_field("researcher")
         return change
 
 
diff --git a/sface/sficreate.py b/sface/sficreate.py
new file mode 100644 (file)
index 0000000..b3de732
--- /dev/null
@@ -0,0 +1,99 @@
+import calendar
+import datetime
+import os
+import re
+import sys
+import time
+
+from PyQt4.QtCore import *
+from PyQt4.QtGui import *
+from sface.config import config
+from sface.sfiprocess import SfiProcess
+
+class CreateWindow(QDialog):
+    def __init__(self, parent=None):
+        super(CreateWindow, self).__init__(parent)
+        self.setWindowTitle("Create new Slice")
+
+        self.renewProcess = None
+
+        self.sliceWasCreated = False
+
+        hrnLabel = QLabel("Slice HRN:")
+        self.hrnEdit = QLineEdit()
+        urlLabel = QLabel("Project URL:")
+        self.urlEdit = QLineEdit()
+        descLabel = QLabel("Description")
+        self.descEdit = QTextEdit()
+
+        self.status = QLabel("")
+
+        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+        self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
+
+        layout = QVBoxLayout()
+        layout.addWidget(hrnLabel)
+        layout.addWidget(self.hrnEdit)
+        layout.addWidget(urlLabel)
+        layout.addWidget(self.urlEdit)
+        layout.addWidget(descLabel)
+        layout.addWidget(self.descEdit)
+        layout.addWidget(self.status)
+        layout.addWidget(self.buttonBox)
+        self.setLayout(layout)
+
+        self.connect(self.buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
+        self.connect(self.buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
+
+    def accept(self):
+        auth = config.getAuthority()
+        desc = str(self.descEdit.toPlainText())
+        hrn = str(self.hrnEdit.text())
+        type = "slice"
+        url = str(self.urlEdit.text())
+
+        if not hrn.startswith(auth):
+            QMessageBox.warning(self, "Invalid HRN", "HRN must be within your current authority (%s)" % auth)
+            return
+
+        if not (url.startswith("http://") or url.startswith("https://")):
+            QMessageBox.warning(self, "Invalid URL", "URL must start with http:// or https://")
+            return
+
+        if not desc:
+            QMessageBox.warning(self, "Invalid Description", "Description is too short")
+            return
+
+        self.setStatus("Registering Slice...")
+
+        self.createProcess = SfiProcess(self)
+        self.connect(self.createProcess, SIGNAL('finished()'), self.createFinished)
+
+        self.buttonBox.setEnabled(False)
+
+        newSliceRecord = os.path.expanduser("~/.sfi/newslice.record")
+        file(newSliceRecord, "w").write('<record authority="%s" description="%s" hrn="%s" type="%s" url="%s"></record>' % (auth, desc, hrn, type, url))
+        self.createProcess.addRecord(newSliceRecord)
+
+    def setStatus(self, x):
+        self.status.setText(x)
+
+    def createFinished(self):
+        faultString = self.createProcess.getFaultString()
+        if not faultString:
+            self.setStatus("<font color='green'>Slice created.</font>")
+            self.sliceWasCreated = True
+            self.buttonBox.setEnabled(True)
+            self.buttonBox.clear()
+            self.buttonBox.addButton(QDialogButtonBox.Close)
+        else:
+            self.setStatus("<font color='red'>Slice creation failed: %s</font>" % (faultString))
+            self.sliceWasCreated = False
+            self.buttonBox.setEnabled(True)
+
+        self.disconnect(self.createProcess, SIGNAL('finished()'), self.createFinished)
+        self.createProcess = None
+
+    def getHrn(self):
+        return self.hrnEdit.text()
+
index e06bf9a..34608de 100644 (file)
@@ -5,7 +5,7 @@ import time
 
 from PyQt4.QtCore import *
 from sface.config import config
-from sface.xmlrpcwindow import XmlrpcTracker
+from sface.xmlrpcwindow import XmlrpcTracker, XmlrpcReader
 
 def find_executable(exec_name):
     """find the given executable in $PATH"""
@@ -28,7 +28,8 @@ class SfiProcess(QObject):
         self.connect(self.process, SIGNAL("finished(int, QProcess::ExitStatus)"),
                      self.processFinished)
 
-        self.xmlrpctracker = XmlrpcTracker()
+        self.xmlrpcreader = XmlrpcReader() # this one is for parsing XMLRPC responses
+        self.xmlrpctracker = XmlrpcTracker() # this one is for the debug window
 
         # holds aggregate output from processStandardOutput(); used by xmlrpc
         # tracker.
@@ -44,9 +45,11 @@ class SfiProcess(QObject):
         self.args << "-d"
         self.args << config.get_dirname()
 
-        if config.debug:
-            # this shows xmlrpc conversation, see sfi.py docs.
-            self.args << QString('-D')
+        # this shows xmlrpc conversation, see sfi.py docs.
+        # always do this, so we can parse the XML result for faults and show
+        # then to the users.
+        self.args << QString('-D')
+
         for arg in args:
             self.args << QString(arg)
 
@@ -83,9 +86,23 @@ class SfiProcess(QObject):
                 print "ReadError"
             elif err == QProcess.UnknownError:
                 print "UnknownError"
+
+        # extract any faults from the XMLRPC response(s)
+        self.xmlrpcreader.responses = []
+        self.xmlrpcreader.store(self.output)
+        self.xmlrpcreader.extractXml()
+        self.responses = self.xmlrpcreader.responses
+        self.faults = [x for x in self.responses if (x["kind"]=="fault")]
+
         self.trace_end()
         self.emit(SIGNAL("finished()"))
 
+    def getFaultString(self):
+        if self.faults == []:
+            return None
+
+        return self.faults[0].get("faultString","") + " (" + self.faults[0].get("faultCode","") + ")"
+
     def __getRSpec(self, mgr):
         slice = config.getSlice()
         # Write RSpec to file for testing.
@@ -174,6 +191,8 @@ class SfiProcess(QObject):
         self.start()
 
     def start(self):
+        self.respones = []
+        self.faults = []
         self.output = ""
         self.trace_command()
         self.process.start(self.exe, self.args)
index 17cd8d6..8da166e 100644 (file)
@@ -6,20 +6,28 @@ import sys
 import time
 
 from PyQt4.QtCore import *
+from PyQt4.QtGui import *
 from sface.config import config
 from sface.sfiprocess import SfiProcess
+#from sface.sfithread import SfiThread
 
 class SfiRenewer(QObject):
     def __init__(self, hrn, newExpiration, parent=None):
         QObject.__init__(self, parent)
         self.hrn = hrn
         self.newExpiration = newExpiration
+        self.faultString = None
 
         self.renewProcess = SfiProcess(self)
         self.connect(self.renewProcess, SIGNAL('finished()'), self.finishedGetRecord)
         self.renewProcess.getRecord(hrn=config.getSlice(), filename="/tmp/slicerecord")
 
     def finishedGetRecord(self):
+        faultString = self.renewProcess.getFaultString()
+        if faultString:
+            self.emitFinished("fault", faultString)
+            return
+
         f = open("/tmp/slicerecord", "r")
         data = f.read()
         f.close()
@@ -43,6 +51,11 @@ class SfiRenewer(QObject):
         self.renewProcess.updateRecord("/tmp/slicerecord")
 
     def finishedUpdateRecord(self):
+        faultString = self.renewProcess.getFaultString()
+        if faultString:
+            self.emitFinished("fault", faultString)
+            return
+
         # we have to force sfi.py to download an updated slice credential
         sliceCredName = config.fullpath("slice_" + self.hrn.split(".")[-1] + ".cred")
         if os.path.exists(sliceCredName):
@@ -56,6 +69,11 @@ class SfiRenewer(QObject):
         self.renewProcess.renewSlivers(self.newExpiration.strftime("%Y-%m-%dT%H:%M:%SZ"))
 
     def finishedRenewSlivers(self):
+        faultString = self.renewProcess.getFaultString()
+        if faultString:
+            self.emitFinished("fault", faultString)
+            return
+
         self.emitFinished("success")
 
     def emitFinished(self, status, statusMsg=None):
@@ -63,3 +81,75 @@ class SfiRenewer(QObject):
         self.statusMsg = statusMsg
         self.emit(SIGNAL("finished()"))
 
+class RenewWindow(QDialog):
+    def __init__(self, parent=None):
+        super(RenewWindow, self).__init__(parent)
+        self.setWindowTitle("Renew Slivers")
+
+        self.renewProcess = None
+
+        self.duration = QComboBox()
+
+        self.expirations = []
+
+        durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
+
+        now = datetime.datetime.utcnow()
+        for (weeks, desc) in durations:
+            exp = now + datetime.timedelta(days = weeks * 7)
+            desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
+            self.expirations.append(exp)
+            self.duration.addItem(desc)
+
+        self.duration.setCurrentIndex(0)
+
+        self.status = QLabel("")
+
+        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+        self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
+
+        layout = QVBoxLayout()
+        layout.addWidget(self.duration)
+        layout.addWidget(self.status)
+        layout.addWidget(self.buttonBox)
+        self.setLayout(layout)
+
+        #self.status.hide()
+
+        self.connect(self.buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
+        self.connect(self.buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
+
+    def accept(self):
+        self.setStatus("Renewing Slice...")
+
+        self.renewProcess = SfiRenewer(config.getSlice(), self.get_new_expiration(), self)
+        self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
+
+        self.duration.setEnabled(False)
+        self.buttonBox.setEnabled(False)
+
+    def setStatus(self, x):
+        self.status.setText(x)
+
+    def renewFinished(self):
+        if self.renewProcess.status == "success":
+            color = "green"
+            # give the user the <close> button
+            self.buttonBox.clear()
+            self.buttonBox.addButton(QDialogButtonBox.Close)
+        else:
+            color = "red"
+
+        if self.renewProcess.statusMsg:
+            self.setStatus("<font color='%s'>Renew %s: %s</font>" % (color, self.renewProcess.status, self.renewProcess.statusMsg))
+        else:
+            self.setStatus("<font color='%s'>Renew %s</font>" % (color, self.renewProcess.status))
+
+        self.buttonBox.setEnabled(True)
+
+        self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
+        self.renewProcess = None
+
+    def get_new_expiration(self):
+        index = self.duration.currentIndex()
+        return self.expirations[index]
index a368a77..81007c0 100644 (file)
@@ -1,10 +1,12 @@
 import re
+from lxml import etree
 from PyQt4.QtXml import QDomDocument
 from sface.xmlwidget import XmlWindow, DomModel
 
-class XmlrpcTracker():
+class XmlrpcReader():
     def __init__(self):
-        self.xmlrpcWindow = XmlrpcWindow()
+        self.rawOutput = None
+        self.responses = []
 
     def getAndPrint(self, rawOutput):
         self.store(rawOutput)
@@ -14,15 +16,52 @@ class XmlrpcTracker():
             # only popup the window if we have something to show
             self.showXmlrpc()
 
-    def showXmlrpc(self):
-        self.xmlrpcWindow.show()
-        self.xmlrpcWindow.resize(500, 640)
-        self.xmlrpcWindow.raise_()
-        self.xmlrpcWindow.activateWindow()
-
     def store(self, rawOutput):
         self.rawOutput = rawOutput
 
+    def parseMethodResponse(self, mr):
+        mr = str(mr) # PyQT supplies a QByteArray; make it a string
+
+        response = {"kind": "unknown"}
+
+        try:
+            tree = etree.fromstring(mr)
+        except etree.XMLSyntaxError, e:
+            print "failed to parse XML response", str(e)
+            #file("badparse.xml","w").write(mr)
+            return response
+
+        if tree.tag != "methodResponse" or (len(list(tree))==0):
+            return response
+
+        # a fault should look like:
+        #     kind: "fault"
+        #     faultCode: "102"
+        #     faultString: "Register: Missing authority..."
+
+        faults = tree.xpath("//methodResponse/fault")
+        for fault in faults:
+            response["kind"] = "fault"
+            structs = fault.xpath("value/struct")
+            for struct in structs:
+                members = struct.xpath("member")
+                for member in members:
+                    names = member.xpath("name")
+                    values = member.xpath("value")
+                    if (names) and (values):
+                        name = names[0]
+                        value = values[0]
+                        if len(list(value))>0:
+                            data = list(value)[0]
+                            response[name.text] = data.text
+            # once we have the first fault, return
+            return response
+
+        # whatever didn't fault must have succeeded?
+        response["kind"] = "success"
+
+        return response
+
     def extractXml(self):
         pttrnAsk = '<methodCall>.*?</methodCall>'
         pttrnAns = '<methodResponse>.*?</methodResponse>'
@@ -30,18 +69,55 @@ class XmlrpcTracker():
         replies = re.compile(pttrnAns, re.DOTALL).findall(self.rawOutput)
         # cleaning
         answers = [ x.replace('\\n','\n') for x in answers ]
-        replies = [ x.replace('\\n','\n').replace("'\nbody: '", '') for x in replies ]
+        replies = [ x.replace('\\n','\n').replace("'\nbody: '", '').replace("\"\nbody: '", '') for x in replies ]
         replies.reverse() # so that I use pop() as popleft
         # A well-formed XML document must have one, and only one, top-level element
-        self.xml = '<debug>'
+
+        self.responses = []
+
+        self.xml = ""
         for ans in answers:
-            self.xml += ans + replies.pop()
-        self.xml += '</debug>'
+            self.xml += ans
+            # we could have less responses than calls, so guard the pop
+            if replies:
+                replyXml = replies.pop()
+                self.xml += replyXml
+                self.responses.append(self.parseMethodResponse(replyXml))
+
+        # just in case somehow we ended up with more responses than calls
+        while replies:
+            replyXml = replies.pop()
+            self.xml += replyXml
+            self.responses.append(self.parseMethodResponse(replyXml))
+
+        return self.xml
 
     def stats(self):
         # statistics: round-trip time, size of the com
         pass
 
+class XmlrpcTracker(XmlrpcReader):
+    def __init__(self):
+        XmlrpcReader.__init__(self)
+        self.xmlrpcWindow = XmlrpcWindow()
+
+    def getAndPrint(self, rawOutput):
+        self.store(rawOutput)
+        self.extractXml()
+        self.xmlrpcWindow.setData(self.xml)
+        if self.xml != "<debug></debug>":
+            # only popup the window if we have something to show
+            self.showXmlrpc()
+
+    def showXmlrpc(self):
+        self.xmlrpcWindow.show()
+        self.xmlrpcWindow.resize(500, 640)
+        self.xmlrpcWindow.raise_()
+        self.xmlrpcWindow.activateWindow()
+
+    def extractXml(self):
+        self.xml = "<debug>" + XmlrpcReader.extractXml(self) + "</debug>"
+
 class XmlrpcWindow(XmlWindow):
     def __init__(self, parent=None):
         # super __init__() calls updateView,