Rewrote using QStandardItemModel
[sface.git] / sface / screens / mainscreen.py
1
2 import os
3 from PyQt4.QtCore import *
4 from PyQt4.QtGui import *
5
6 from sfa.util.rspecHelper import RSpec
7 from sface.config import config
8 from sface.sfiprocess import SfiProcess
9 from sface.screens.sfascreen import SfaScreen
10
11 already_in_nodes = []
12
13 node_status = { "in": "Already Selected",
14                 "out": "Not Selected",
15                 "add": "To be Added",
16                 "remove": "To be Removed"}
17
18 tag_status = { "in": "Already Set",
19                 "out": "Not Set",
20                 "add": "To be Added",
21                 "remove": "To be Removed"}
22
23 default_tags = "Default tags"
24 settable_tags = ['delegations', 'initscript']
25
26 def itemType(index):
27     if index.parent().parent().isValid():
28         return "tag"
29     else:
30         return "node"
31
32
33 class NodeView(QTreeView):
34     def __init__(self, parent):
35         QTreeView.__init__(self, parent)
36
37         self.setAnimated(True)
38         self.setItemsExpandable(True)
39         self.setRootIsDecorated(True)
40         self.setAlternatingRowColors(True)
41 #        self.setSelectionMode(self.MultiSelection)
42         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
43         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
44         self.setToolTip("Double click on a row to change its status.  Right click on a host to add a tag.")
45
46     def mouseDoubleClickEvent(self, event):
47         index = self.currentIndex()
48         model = index.model()
49         status_index = model.index(index.row(), 1, index.parent())
50         status_data = status_index.data().toString()
51         node_index = model.index(index.row(), 0, index.parent())
52         node_data = node_index.data().toString()
53
54         if itemType(node_index) == "tag":
55             data = node_index.data().toString()
56             tagname, value = data.split(": ")
57             if tagname not in settable_tags:
58                 # Pop up error msg
59                 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
60                 return
61             if status_data == tag_status['in']:
62                 model.setData(status_index, QString(tag_status['remove']))
63             elif status_data == tag_status['add']:
64                 model.setData(status_index, QString(tag_status['out']))
65             elif status_data == tag_status['remove']:
66                 model.setData(status_index, QString(tag_status['in']))
67             else: model.setData(status_index, QString(node_status['out']))
68         else:
69             # This is a hostname
70             if status_data == node_status['in']:
71                 model.setData(status_index, QString(node_status['remove']))
72             elif status_data == node_status['out']:
73                 model.setData(status_index, QString(node_status['add']))
74             elif status_data in (node_status['add'], node_status['remove']):
75                 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
76                 else: model.setData(status_index, QString(node_status['out']))
77
78         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
79
80     def mousePressEvent(self, event):
81         QTreeView.mousePressEvent(self, event)
82         if event.button() == Qt.LeftButton:
83             return
84
85         # Right click
86         index = self.currentIndex()
87         model = index.model()
88         status_index = model.index(index.row(), 1, index.parent())
89         status_data = status_index.data().toString()
90         node_index = model.index(index.row(), 0, index.parent())
91         node_data = node_index.data().toString()
92
93         if itemType(node_index) == "node":
94             # This is a hostname
95             if status_data in (node_status['in'], node_status['add'], ""):
96                 # Pop up a dialog box for adding a new attribute
97                 tagname, ok = QInputDialog.getItem(self, "Add tag",
98                                                    "Tag name:", settable_tags)
99                 if ok:
100                     value, ok = QInputDialog.getText(self, "Add tag",
101                                                      "Value for tag '%s'" % tagname)
102                     if ok:
103                         # Add a new row to the model for the tag
104
105                         # For testing with the QStandardItemModel
106                         #nodeItem = model.itemFromIndex(index)
107                         #tagstring = QString("%s: %s" % (tagname, value))
108                         #tagItem = QStandardItem(tagstring)
109                         #status = QStandardItem(QString(tag_status['add']))
110                         #nodeItem.appendRow([tagItem, status])
111
112                         # We're using the QSortFilterProxyModel here
113                         src_index = model.mapToSource(index)
114                         src_model = src_index.model()
115                         nodeItem = src_model.itemFromIndex(src_index)
116                         tagstring = QString("%s: %s" % (tagname, value))
117                         tagItem = QStandardItem(tagstring)
118                         status = QStandardItem(QString(tag_status['add']))
119                         nodeItem.appendRow([tagItem, status])
120
121             elif status_data in (node_status['out'], node_status['remove']):
122                 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
123                 return
124
125         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
126
127     def currentChanged(self, current, previous):
128         model = current.model()
129         node_index = model.index(current.row(), 0, current.parent())
130         node_data = node_index.data().toString()
131         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
132         
133                 
134
135 class NodeNameDelegate(QStyledItemDelegate):
136     def __init__(self, parent):
137         QStyledItemDelegate.__init__(self, parent)
138
139     def paint(self, painter, option, index):
140         model = index.model()
141         status_index = model.index(index.row(), 1, index.parent())
142         status_data = status_index.data().toString()
143
144         fm = QFontMetrics(option.font)
145         rect = option.rect
146
147         data = index.data().toString()
148         rect.setHeight(rect.height() - 2)
149         rect.setWidth(fm.width(QString(data)) + 6)
150         rect.setX(rect.x() + 5)
151         rect.setY(rect.y() - 1)
152
153         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
154
155         path = QPainterPath()
156         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
157
158         painter.save()
159         painter.setRenderHint(QPainter.Antialiasing)
160
161         if itemType(index) == "node":
162             if status_data == node_status['in']: # already in the slice
163                 painter.fillPath(path, QColor("cyan"))
164                 painter.setPen(QColor.fromRgb(0, 0, 0))
165                 painter.drawText(option.rect, 0, QString(data))
166
167             elif status_data == node_status['add']: # newly added to the slice
168                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
169                 painter.setPen(QColor.fromRgb(0, 0, 0))
170                 painter.drawText(option.rect, 0, QString(data))
171
172             elif status_data == node_status['remove']: # removed from the slice
173                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
174                 painter.setPen(QColor.fromRgb(0, 0, 0))
175                 painter.drawText(option.rect, 0, QString(data))
176
177             else:
178                 painter.setPen(QColor.fromRgb(0, 0, 0))
179                 painter.drawText(option.rect, 0, QString(data))
180
181         else:
182             if status_data == tag_status['in']: # already in the slice
183                 painter.fillPath(path, QColor("cyan"))
184                 painter.setPen(QColor.fromRgb(0, 0, 0))
185                 painter.drawText(option.rect, 0, QString(data))
186
187             elif status_data == tag_status['add']: # newly added to the slice
188                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
189                 painter.setPen(QColor.fromRgb(0, 0, 0))
190                 painter.drawText(option.rect, 0, QString(data))
191
192             elif status_data == tag_status['remove']: # removed from the slice
193                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
194                 painter.setPen(QColor.fromRgb(0, 0, 0))
195                 painter.drawText(option.rect, 0, QString(data))
196
197             else:
198                 painter.setPen(QColor.fromRgb(0, 0, 0))
199                 painter.drawText(option.rect, 0, QString(data))
200
201         painter.restore()
202
203 class SliceWidget(QWidget):
204     def __init__(self, parent):
205         QWidget.__init__(self, parent)
206
207         self.network_names = []
208         self.process = SfiProcess(self)
209
210         self.slicename = QLabel("", self)
211         self.updateSliceName()
212         self.slicename.setScaledContents(False)
213         searchlabel = QLabel ("Search: ", self)
214         searchlabel.setScaledContents(False)
215         searchbox = QLineEdit(self)
216         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
217
218         toplayout = QHBoxLayout()
219         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
220         toplayout.addStretch()
221         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
222         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
223
224         self.nodeView = NodeView(self)
225         self.nodeModel = QStandardItemModel(0, 2, self)
226         self.filterModel = QSortFilterProxyModel(self) # enable filtering
227
228         self.nodeNameDelegate = NodeNameDelegate(self)
229
230         refresh = QPushButton("Update Slice Data", self)
231         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
232         submit = QPushButton("Submit", self)
233         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
234
235         bottomlayout = QHBoxLayout()
236         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
237         bottomlayout.addStretch()
238         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
239
240         layout = QVBoxLayout()
241         layout.addLayout(toplayout)
242         layout.addWidget(self.nodeView)
243         layout.addLayout(bottomlayout)
244         self.setLayout(layout)
245         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
246
247         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
248         self.connect(submit, SIGNAL('clicked()'), self.submit)
249         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
250         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
251                      self.nodeSelectionChanged)
252
253         self.updateView()
254
255     def submitFinished(self):
256         self.setStatus("<font color='green'>Slice data submitted.</font>")
257         QTimer.singleShot(1000, self.refresh)
258
259     def refreshFinished(self):
260         self.setStatus("<font color='green'>Slice data updated.</font>", timeout=5000)
261         self.updateView()
262         self.parent().signalAll("rspecUpdated")
263
264     def readSliceRSpec(self):
265         rspec_file = config.getSliceRSpecFile()
266         if os.path.exists(rspec_file):
267             xml = open(rspec_file).read()
268             return RSpec(xml)
269         return None
270
271     def setStatus(self, msg, timeout=None):
272         self.parent().setStatus(msg, timeout)
273
274     def checkRunningProcess(self):
275         if self.process.isRunning():
276             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
277             return True
278         return False
279
280     def filter(self, filter_string):
281         # for hierarchical models QSortFilterProxyModel applies the
282         # sort recursively. if the parent doesn't match the criteria
283         # we won't be able to match the children. so we need to match
284         # parent (by matching the network_names)
285         networks = ["^%s$" % n for n in self.network_names]
286         filters = networks + [str(filter_string)]
287         self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
288
289     def itemStatus(self, item):
290         statusItem = item.parent().child(item.row(), 1)
291         return statusItem.data(Qt.DisplayRole).toString()
292
293     def itemText(self, item):
294         return item.data(Qt.DisplayRole).toString()
295
296     # Recursively walk the tree, making changes to the RSpec
297     def process_subtree(self, rspec, item, depth = 0):
298         change = False
299         model = self.nodeModel
300
301         if depth in [0, 1]:
302             pass
303         elif depth == 2: # Hostname
304             hostname = self.itemText(item)
305             testbed = self.itemText(item.parent())
306             status = self.itemStatus(item)
307             if status == node_status['add']:
308                 print "Add hostname: %s" % hostname
309                 rspec.add_sliver(hostname, testbed)
310                 change = True
311             elif status == node_status['remove']:
312                 print "Remove hostname: %s" % hostname
313                 rspec.remove_sliver(hostname, testbed)
314                 change = True
315         elif depth == 3: # Tag
316             tag, value = self.itemText(item).split(": ")
317             status = self.itemStatus(item)
318             tag = "%s" % tag     # Prevent weird error from lxml
319             value = "%s" % value # Prevent weird error from lxml
320             node = self.itemText(item.parent())
321             testbed = self.itemText(item.parent().parent())
322             if status == tag_status['add']:
323                 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
324                 if node.startsWith(default_tags):
325                     rspec.add_default_sliver_attribute(tag, value, testbed)
326                 else:
327                     rspec.add_sliver_attribute(node, tag, value, testbed)
328                 change = True
329             elif status == tag_status['remove']:
330                 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
331                 if node.startsWith(default_tags):
332                     rspec.remove_default_sliver_attribute(tag, value, testbed)
333                 else:
334                     rspec.remove_sliver_attribute(node, tag, value, testbed)
335                 change = True
336
337         children = item.rowCount()
338         for row in range(0, children):
339             status = self.process_subtree(rspec, item.child(row), depth + 1)
340             change = change or status
341
342         return change
343
344     def submit(self):
345         if self.checkRunningProcess():
346             return
347
348         rspec = self.readSliceRSpec()
349         change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
350
351         if not change:
352             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
353             return
354
355         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
356         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
357
358         self.process.applyRSpec(rspec)
359         self.setStatus("Sending slice data (RSpec). This will take some time...")
360         
361
362     def refresh(self):
363         if not config.getSlice():
364             self.setStatus("<font color='red'>Slice not set yet!</font>")
365             return
366
367         if self.process.isRunning():
368             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
369             return
370
371         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
372         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
373
374         self.process.getRSpecFromSM()
375         self.setStatus("Updating slice data. This will take some time...")
376
377     def updateView(self):
378         global already_in_nodes
379         already_in_nodes = []
380         self.network_names = []
381         self.nodeModel.clear()
382
383         rspec = self.readSliceRSpec()
384         if not rspec:
385             return None
386
387         rootItem = self.nodeModel.invisibleRootItem()
388         networks = rspec.get_network_list()
389         for network in networks:
390             self.network_names.append(network)
391             networkItem = QStandardItem(QString(network))
392             rootItem.appendRow([networkItem, QStandardItem(QString(""))])
393
394             all_nodes = rspec.get_node_list(network)
395             sliver_nodes = rspec.get_sliver_list(network)
396             available_nodes = filter(lambda x:x not in sliver_nodes, all_nodes)
397
398             already_in_nodes += sliver_nodes
399
400             # Add default slice tags
401             nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
402             statusItem = QStandardItem(QString(""))
403             networkItem.appendRow([nodeItem, statusItem])
404             attrs = rspec.get_default_sliver_attributes(network)
405             for (name, value) in attrs:
406                     tagstring = QString("%s: %s" % (name, value))
407                     tagItem = QStandardItem(tagstring)
408                     status = QStandardItem(QString(tag_status['in']))
409                     nodeItem.appendRow([tagItem, status])
410
411             for node in sliver_nodes:
412                 nodeItem = QStandardItem(QString(node))
413                 statusItem = QStandardItem(QString(node_status['in']))
414                 networkItem.appendRow([nodeItem, statusItem])
415
416                 attrs = rspec.get_sliver_attributes(node, network)
417                 for (name, value) in attrs:
418                     tagstring = QString("%s: %s" % (name, value))
419                     tagItem = QStandardItem(tagstring)
420                     statusItem = QStandardItem(QString(tag_status['in']))
421                     nodeItem.appendRow([tagItem, statusItem])
422
423             for node in available_nodes:
424                 nodeItem = QStandardItem(QString(node))
425                 statusItem = QStandardItem(QString(node_status['out']))
426                 networkItem.appendRow([nodeItem, statusItem])
427
428         self.filterModel.setSourceModel(self.nodeModel)
429         self.filterModel.setFilterKeyColumn(-1)
430         self.filterModel.setDynamicSortFilter(True)
431
432         headers = QStringList() << "Hostname or Tag" << "Status"
433         self.nodeModel.setHorizontalHeaderLabels(headers)
434
435         self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
436         self.nodeView.setModel(self.filterModel)
437         self.nodeView.expandAll()
438         self.nodeView.resizeColumnToContents(0)
439         self.nodeView.collapseAll()
440
441     def updateSliceName(self):
442         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
443
444     def nodeSelectionChanged(self, hostname):
445         self.parent().nodeSelectionChanged(hostname)
446
447 class MainScreen(SfaScreen):
448     def __init__(self, parent):
449         SfaScreen.__init__(self, parent)
450
451         slice = SliceWidget(self)
452         self.init(slice, "Main Window", "OneLab Federation GUI")
453
454     def rspecUpdated(self):
455         self.mainwin.rspecWindow.updateView()
456         
457     def configurationChanged(self):
458         self.widget.updateSliceName()
459         self.widget.updateView()
460         self.mainwin.rspecWindow.updateView()
461
462     def nodeSelectionChanged(self, hostname):
463         self.mainwin.nodeSelectionChanged(hostname)