From: Thierry Parmentelat Date: Thu, 8 Sep 2011 12:25:19 +0000 (+0200) Subject: Merge branch 'master' of ssh://git.onelab.eu/git/sface X-Git-Tag: sface-0.1-19~13 X-Git-Url: http://git.onelab.eu/?p=sface.git;a=commitdiff_plain;h=1a0bb6c8cf51c8abcffdc94da1ba4d9c9bb452e0;hp=ab5bd9742a2d1b306e1a65b7f37c46f602c3fdd3 Merge branch 'master' of ssh://git.onelab.eu/git/sface --- diff --git a/sface.spec b/sface.spec index b5914cf..257b302 100644 --- a/sface.spec +++ b/sface.spec @@ -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 - 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 - 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. diff --git a/sface/screens/configscreen.py b/sface/screens/configscreen.py index dc64620..37db423 100644 --- a/sface/screens/configscreen.py +++ b/sface/screens/configscreen.py @@ -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+='/' diff --git a/sface/screens/mainscreen.py b/sface/screens/mainscreen.py index e6e4fb4..5578fbc 100644 --- a/sface/screens/mainscreen.py +++ b/sface/screens/mainscreen.py @@ -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) diff --git a/sface/screens/userscreen.py b/sface/screens/userscreen.py index bcb87f0..9adf116 100644 --- a/sface/screens/userscreen.py +++ b/sface/screens/userscreen.py @@ -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 index 0000000..b3de732 --- /dev/null +++ b/sface/sficreate.py @@ -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('' % (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("Slice created.") + self.sliceWasCreated = True + self.buttonBox.setEnabled(True) + self.buttonBox.clear() + self.buttonBox.addButton(QDialogButtonBox.Close) + else: + self.setStatus("Slice creation failed: %s" % (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() + diff --git a/sface/sfiprocess.py b/sface/sfiprocess.py index e06bf9a..34608de 100644 --- a/sface/sfiprocess.py +++ b/sface/sfiprocess.py @@ -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) diff --git a/sface/sfirenew.py b/sface/sfirenew.py index 17cd8d6..8da166e 100644 --- a/sface/sfirenew.py +++ b/sface/sfirenew.py @@ -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 button + self.buttonBox.clear() + self.buttonBox.addButton(QDialogButtonBox.Close) + else: + color = "red" + + if self.renewProcess.statusMsg: + self.setStatus("Renew %s: %s" % (color, self.renewProcess.status, self.renewProcess.statusMsg)) + else: + self.setStatus("Renew %s" % (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] diff --git a/sface/xmlrpcwindow.py b/sface/xmlrpcwindow.py index a368a77..81007c0 100644 --- a/sface/xmlrpcwindow.py +++ b/sface/xmlrpcwindow.py @@ -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 = '.*?' pttrnAns = '.*?' @@ -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 = '' + + self.responses = [] + + self.xml = "" for ans in answers: - self.xml += ans + replies.pop() - self.xml += '' + 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 != "": + # 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 = "" + XmlrpcReader.extractXml(self) + "" + class XmlrpcWindow(XmlWindow): def __init__(self, parent=None): # super __init__() calls updateView,