deleteslivers for the emulab aggregate
[sface.git] / sface / xmlrpcwindow.py
1 import re
2 import xmlrpclib
3 from lxml import etree
4 from PyQt4.QtXml import QDomDocument
5 from sface.xmlwidget import XmlWindow, DomModel, XmlView, XmlDelegate, ElemNodeDelegate, TextNodeDelegate, CommentNodeDelegate
6
7 from PyQt4.QtCore import *
8 from PyQt4.QtGui import *
9
10 class XmlrpcReader():
11     def __init__(self):
12         self.rawOutput = None
13         self.responses = []
14         self.calls = []
15
16     def store(self, rawOutput):
17         self.rawOutput = rawOutput
18
19     def parseMethodCall(self, mc):
20         mc = str(mc)
21
22         request = {}
23
24         try:
25             tree = etree.fromstring(mc)
26         except etree.XMLSyntaxError, e:
27             return request
28
29         if tree.tag != "methodCall" or (len(list(tree))==0):
30             return request
31
32         try:
33             (pythonParams, methodName) = xmlrpclib.loads(etree.tostring(tree))
34         except:  # in case something went wrong when unmarashaling...
35             pythonParams = []
36             methodName = "<exception in parseMethodCall>"
37
38         request["args"] = [str(x) for x in pythonParams]
39         request["methodName"] = methodName
40
41         return request
42
43     def parseMethodResponse(self, mr):
44         mr = str(mr) # PyQT supplies a QByteArray; make it a string
45
46         response = {"kind": "unknown"}
47
48         try:
49             tree = etree.fromstring(mr)
50         except etree.XMLSyntaxError, e:
51             print "failed to parse XML response", str(e)
52             #file("badparse.xml","w").write(mr)
53             return response
54
55         if tree.tag != "methodResponse" or (len(list(tree))==0):
56             return response
57
58         # a fault should look like:
59         #     kind: "fault"
60         #     faultCode: "102"
61         #     faultString: "Register: Missing authority..."
62
63         faults = tree.xpath("//methodResponse/fault")
64         for fault in faults:
65             response["kind"] = "fault"
66             structs = fault.xpath("value/struct")
67             for struct in structs:
68                 members = struct.xpath("member")
69                 for member in members:
70                     names = member.xpath("name")
71                     values = member.xpath("value")
72                     if (names) and (values):
73                         name = names[0]
74                         value = values[0]
75                         if len(list(value))>0:
76                             data = list(value)[0]
77                             response[name.text] = data.text
78             # once we have the first fault, return
79             return response
80
81         # whatever didn't fault must have succeeded?
82         # "success" really isn't quite the right word. The best we can say is
83         # that the call was executed. The actual op may or may not have
84         # succeeded.
85         response["kind"] = "executed"
86
87         return response
88
89     def extractXml(self):
90         pttrnAsk = '<methodCall>.*?</methodCall>'
91         pttrnAns = '<methodResponse>.*?</methodResponse>'
92         requests = re.compile(pttrnAsk, re.DOTALL).findall(self.rawOutput)
93         replies = re.compile(pttrnAns, re.DOTALL).findall(self.rawOutput)
94         # cleaning
95         requests = [ x.replace('\\n','\n') for x in requests ]
96         replies = [ x.replace('\\n','\n').replace("'\nbody: '", '').replace("\"\nbody: '", '') for x in replies ]
97         # A well-formed XML document must have one, and only one, top-level element
98
99         self.responses = []
100
101         self.xml = ""
102         for requestXml in requests:
103             self.xml += requestXml
104             callDict = {"request": requestXml}
105             # we could have less responses than calls, so guard the pop
106             if replies:
107                 replyXml = replies.pop(0)
108                 self.xml += replyXml
109
110                 # parse the response to extract fault information
111                 responseDict = self.parseMethodResponse(replyXml)
112                 self.responses.append(responseDict)
113
114                 requestDict = self.parseMethodCall(requestXml)
115
116                 callDict["reply"] = replyXml
117                 callDict.update(responseDict)
118                 callDict.update(requestDict)
119
120             self.calls.append(callDict)
121
122         # just in case somehow we ended up with more responses than calls
123         while replies:
124             replyXml = replies.pop()
125             self.xml += replyXml
126             self.responses.append(self.parseMethodResponse(replyXml))
127
128         return self.xml
129
130     def stats(self):
131         # statistics: round-trip time, size of the com
132         pass
133
134 class XmlrpcTracker(QWidget):
135     def __init__(self, parent=None):
136         QWidget.__init__(self, parent=parent)
137
138         self.reader = XmlrpcReader()
139
140         labelCalls = QLabel("Calls: (from most recent to least recent)")
141
142         self.callTable = QTableWidget(self)
143         self.callTable.setSelectionBehavior(QAbstractItemView.SelectRows)
144         self.callTable.setSelectionMode(QAbstractItemView.SingleSelection)
145         self.callTable.setMaximumHeight(140)
146
147         labelArgs = QLabel("Params:")
148
149         self.argsTable = QTableWidget(self)
150         self.argsTable.setMaximumHeight(140)
151
152         labelXml = QLabel("XMLRPC Contents")
153         self.xmlView = XmlView(self)
154
155         self.delegate = XmlDelegate(self)
156         self.delegate.insertNodeDelegate('element', ElemNodeDelegate())
157         self.delegate.insertNodeDelegate('text', TextNodeDelegate())
158         self.delegate.insertNodeDelegate('comment', CommentNodeDelegate())
159         self.xmlView.setItemDelegate(self.delegate)
160
161         exportButton = QPushButton("&Export Call")
162         layoutButtons = QHBoxLayout()
163         layoutButtons.addWidget(exportButton)
164         layoutButtons.addStretch()
165
166         self.layout = QVBoxLayout()
167         self.layout.addWidget(labelCalls)
168         self.layout.addWidget(self.callTable)
169         self.layout.addWidget(labelArgs)
170         self.layout.addWidget(self.argsTable)
171         self.layout.addWidget(labelXml)
172         self.layout.addWidget(self.xmlView)
173         self.layout.addLayout(layoutButtons)
174
175         self.setLayout(self.layout)
176
177         self.connect(self.callTable, SIGNAL("itemSelectionChanged ()"), self.onCallSelect)
178         self.connect(exportButton, SIGNAL("clicked()"), self.onExportClicked)
179
180     def getAndPrint(self, rawOutput):
181         self.reader.store(rawOutput)
182         self.reader.extractXml()
183         self.updateCallTable()
184
185     def updateCallTable(self):
186         self.callTable.clear()
187         self.callTable.setColumnCount(3)
188         self.callTable.setHorizontalHeaderLabels(["name", "status", "faultString"])
189
190         calls = self.reader.calls
191         self.callTable.setRowCount(len(calls))
192
193         row = 0
194         reverse_calls = list(enumerate(calls))
195         reverse_calls.reverse()
196         for (index,call) in reverse_calls:
197             item = QTableWidgetItem(call.get("methodName", "unknown"))
198             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
199             item.setData(Qt.UserRole, index)
200             self.callTable.setItem(row, 0, item)
201
202             item = QTableWidgetItem(call.get("kind", "unknown"))
203             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
204             item.setData(Qt.UserRole, index)
205             self.callTable.setItem(row, 1, item)
206
207             item = QTableWidgetItem(call.get("faultString", ""))
208             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
209             item.setData(Qt.UserRole, index)
210             self.callTable.setItem(row, 2, item)
211
212             row = row + 1
213
214         self.callTable.resizeColumnsToContents()
215
216     def updateArgsTable(self, args):
217         self.argsTable.clear()
218         self.argsTable.setColumnCount(1)
219         self.argsTable.setHorizontalHeaderLabels(["param"])
220         self.argsTable.horizontalHeader().hide()
221         self.argsTable.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
222
223         self.argsTable.setRowCount(len(args))
224
225         row = 0
226         for arg in args:
227             if len(arg)>255:
228                 arg = arg[:255] + "..."
229
230             item = QTableWidgetItem(arg)
231             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
232             self.argsTable.setItem(row, 0, item)
233
234             row = row + 1
235
236         self.argsTable.resizeColumnsToContents()
237
238     def getSelectedCall(self):
239         selItems = self.callTable.selectedItems()
240         if (len(selItems) <= 0):
241             return None
242
243         row = selItems[0].data(Qt.UserRole).toInt()[0]
244
245         calls = self.reader.calls
246         if len(calls)<=row:
247             return None
248
249         call = calls[row]
250
251         return call
252
253     def onCallSelect(self):
254         call = self.getSelectedCall()
255
256         if not call:
257             return
258
259         xml = "<debug>" + call.get("request","") + call.get("reply", "") + "</debug>"
260         #xml = call.get("request","") + call.get("reply", "")
261
262         self.displayXml(xml)
263
264         self.updateArgsTable(call.get("args",[]))
265
266     def onExportClicked(self):
267         call = self.getSelectedCall()
268
269         if not call:
270             return
271
272         filename = QFileDialog.getSaveFileName(self, 'Save File', '.')
273
274         f = open(filename, "w")
275         f.write(call.get("request","") + "\n")
276         f.write(call.get("reply","") + "\n")
277         f.close()
278
279
280     def displayXml(self, xml):
281         self.document = QDomDocument("XMLRPC Tracker")
282         self.document.setContent(xml)
283         self.model = DomModel(self.document, self)
284         self.xmlView.setModel(self.model)
285
286
287 # There's one global tracker for XMLRPCs. Use init_tracker() to create it, and
288 # get_tracker() to get it.
289
290 glo_tracker = None
291
292 def init_tracker(parent):
293     global glo_tracker
294
295     glo_tracker = XmlrpcTracker(parent)
296
297     return glo_tracker
298
299 def get_tracker():
300     global glo_tracker
301
302     return glo_tracker
303
304