display faults in mainscreen
[sface.git] / sface / screens / mainscreen.py
1
2 import datetime
3 import os
4 from PyQt4.QtCore import *
5 from PyQt4.QtGui import *
6
7 #from sfa.util.rspecHelper import RSpec
8 from sfa.rspecs.rspec_parser import parse_rspec
9 from sface.config import config
10 from sface.sfirenew import RenewWindow
11 from sface.sfiprocess import SfiProcess
12 from sface.screens.sfascreen import SfaScreen
13
14 already_in_nodes = []
15
16 node_status = { "in": "Already Selected",
17                 "out": "Not Selected",
18                 "add": "To be Added",
19                 "remove": "To be Removed"}
20
21 tag_status = { "in": "Already Set",
22                 "out": "Not Set",
23                 "add": "To be Added",
24                 "remove": "To be Removed"}
25
26 default_tags = "Default tags"
27 settable_tags = ['delegations', 'initscript']
28
29 NAME_COLUMN = 0
30 NODE_STATUS_COLUMN = 1
31 MEMBERSHIP_STATUS_COLUMN = 2
32 KIND_COLUMN = 3
33
34 # maximum length of a name to display before clipping
35 NAME_MAX_LEN = 48
36
37 def itemType(index):
38     if index.parent().parent().isValid():
39         return "tag"
40     else:
41         return "node"
42
43
44 class NodeView(QTreeView):
45     def __init__(self, parent):
46         QTreeView.__init__(self, parent)
47
48         self.setAnimated(True)
49         self.setItemsExpandable(True)
50         self.setRootIsDecorated(True)
51         self.setAlternatingRowColors(True)
52 #        self.setSelectionMode(self.MultiSelection)
53         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
54         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
55         self.setToolTip("Double click on a row to change its status.  Right click on a host to add a tag.")
56
57     def mouseDoubleClickEvent(self, event):
58         index = self.currentIndex()
59         model = index.model()
60         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
61         status_data = status_index.data().toString()
62         node_index = model.index(index.row(), NAME_COLUMN, index.parent())
63         node_data = node_index.data().toString()
64
65         if itemType(node_index) == "tag":
66             data = node_index.data().toString()
67             tagname, value = data.split(": ")
68             if tagname not in settable_tags:
69                 # Pop up error msg
70                 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
71                 return
72             if status_data == tag_status['in']:
73                 model.setData(status_index, QString(tag_status['remove']))
74             elif status_data == tag_status['add']:
75                 model.setData(status_index, QString(tag_status['out']))
76             elif status_data == tag_status['remove']:
77                 model.setData(status_index, QString(tag_status['in']))
78             else: model.setData(status_index, QString(node_status['out']))
79         else:
80             # This is a hostname
81             if status_data == node_status['in']:
82                 model.setData(status_index, QString(node_status['remove']))
83             elif status_data == node_status['out']:
84                 model.setData(status_index, QString(node_status['add']))
85             elif status_data in (node_status['add'], node_status['remove']):
86                 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
87                 else: model.setData(status_index, QString(node_status['out']))
88
89         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
90
91     def mousePressEvent(self, event):
92         QTreeView.mousePressEvent(self, event)
93         if event.button() == Qt.LeftButton:
94             return
95
96         # Right click
97         index = self.currentIndex()
98         model = index.model()
99         status_index = model.index(index.row(), 1, index.parent())
100         status_data = status_index.data().toString()
101         node_index = model.index(index.row(), 0, index.parent())
102         node_data = node_index.data().toString()
103
104         if itemType(node_index) == "node":
105             # This is a hostname
106             if status_data in (node_status['in'], node_status['add'], ""):
107                 # Pop up a dialog box for adding a new attribute
108                 tagname, ok = QInputDialog.getItem(self, "Add tag",
109                                                    "Tag name:", settable_tags)
110                 if ok:
111                     value, ok = QInputDialog.getText(self, "Add tag",
112                                                      "Value for tag '%s'" % tagname)
113                     if ok:
114                         # Add a new row to the model for the tag
115
116                         # For testing with the QStandardItemModel
117                         #nodeItem = model.itemFromIndex(index)
118                         #tagstring = QString("%s: %s" % (tagname, value))
119                         #tagItem = QStandardItem(tagstring)
120                         #status = QStandardItem(QString(tag_status['add']))
121                         #nodeItem.appendRow([tagItem, status])
122
123                         # We're using the QSortFilterProxyModel here
124                         src_index = model.mapToSource(index)
125                         src_model = src_index.model()
126                         nodeItem = src_model.itemFromIndex(src_index)
127                         tagstring = QString("%s: %s" % (tagname, value))
128                         tagItem = QStandardItem(tagstring)
129                         status = QStandardItem(QString(tag_status['add']))
130                         nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
131
132             elif status_data in (node_status['out'], node_status['remove']):
133                 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
134                 return
135
136         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
137
138     def currentChanged(self, current, previous):
139         model = current.model()
140         node_index = model.index(current.row(), 0, current.parent())
141         node_data = node_index.data().toString()
142         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
143         
144                 
145
146 class NodeNameDelegate(QStyledItemDelegate):
147     def __init__(self, parent):
148         QStyledItemDelegate.__init__(self, parent)
149
150     def displayText(self, value, locale):
151         data = str(QStyledItemDelegate.displayText(self, value, locale))
152         if (len(data)>NAME_MAX_LEN):
153             data = data[:(NAME_MAX_LEN-3)] + "..."
154         return QString(data)
155
156     def paint(self, painter, option, index):
157         model = index.model()
158         data = str(self.displayText(index.data(), QLocale()))
159         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
160         status_data = status_index.data().toString()
161
162         fm = QFontMetrics(option.font)
163         rect = QRect(option.rect)
164
165         rect.setHeight(rect.height() - 2)
166         rect.setWidth(fm.width(QString(data)) + 6)
167         rect.setX(rect.x() + 5)
168         rect.setY(rect.y() - 1)
169
170         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
171
172         path = QPainterPath()
173         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
174
175         painter.save()
176         painter.setRenderHint(QPainter.Antialiasing)
177
178         if option.state & QStyle.State_Selected:
179             painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
180
181         if itemType(index) == "node":
182             if status_data == node_status['in']: # already in the slice
183                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
184                 painter.setPen(QColor.fromRgb(0, 0, 0))
185                 painter.drawText(rect, 0, QString(data))
186
187             elif status_data == node_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(rect, 0, QString(data))
191
192             elif status_data == node_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(rect, 0, QString(data))
196
197             else:
198                 painter.setPen(QColor.fromRgb(0, 0, 0))
199                 painter.drawText(rect, 0, QString(data))
200
201         else:
202             if status_data == tag_status['in']: # already in the slice
203                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
204                 painter.setPen(QColor.fromRgb(0, 0, 0))
205                 painter.drawText(rect, 0, QString(data))
206
207             elif status_data == tag_status['add']: # newly added to the slice
208                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
209                 painter.setPen(QColor.fromRgb(0, 0, 0))
210                 painter.drawText(rect, 0, QString(data))
211
212             elif status_data == tag_status['remove']: # removed from the slice
213                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
214                 painter.setPen(QColor.fromRgb(0, 0, 0))
215                 painter.drawText(rect, 0, QString(data))
216
217             else:
218                 painter.setPen(QColor.fromRgb(0, 0, 0))
219                 painter.drawText(rect, 0, QString(data))
220
221         painter.restore()
222
223 class NodeStatusDelegate(QStyledItemDelegate):
224     def __init__(self, parent):
225         QStyledItemDelegate.__init__(self, parent)
226
227     def paint(self, painter, option, index):
228         model = index.model()
229         nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
230         nodestatus_data = nodestatus_index.data().toString()
231
232         fm = QFontMetrics(option.font)
233         rect = QRect(option.rect)
234
235         data = index.data().toString()
236         rect.setHeight(rect.height() - 2)
237         rect.setWidth(fm.width(QString(data)) + 6)
238         rect.setX(rect.x() + 5)
239         rect.setY(rect.y() - 1)
240
241         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
242
243         path = QPainterPath()
244         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
245
246         painter.save()
247         painter.setRenderHint(QPainter.Antialiasing)
248
249         if option.state & QStyle.State_Selected:
250             painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
251
252         if (nodestatus_data == ""):
253                 painter.setPen(QColor.fromRgb(0, 0, 0))
254                 painter.drawText(rect, 0, QString(data))
255         elif (nodestatus_data == "boot"):
256                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
257                 painter.setPen(QColor.fromRgb(0, 0, 0))
258                 painter.drawText(rect, 0, QString(data))
259         else:
260                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
261                 painter.setPen(QColor.fromRgb(0, 0, 0))
262                 painter.drawText(rect, 0, QString(data))
263
264         painter.restore()
265
266 class NodeFilterProxyModel(QSortFilterProxyModel):
267     def __init__(self, parent=None):
268         QSortFilterProxyModel.__init__(self, parent)
269         self.hostname_filter_regex = None
270         self.nodestatus_filter = None
271
272     def setHostNameFilter(self, hostname):
273         self.hostname_filter_regex = QRegExp(hostname)
274         self.invalidateFilter()
275
276     def setNodeStatusFilter(self, status):
277         if (status == "all"):
278             self.nodestatus_filter = None
279         else:
280             self.nodestatus_filter = status
281         self.invalidateFilter()
282
283     def filterAcceptsRow(self, sourceRow, source_parent):
284         kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
285         if (kind_data == "node"):
286             if self.hostname_filter_regex:
287                 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
288                 if (self.hostname_filter_regex.indexIn(name_data) < 0):
289                     return False
290             if self.nodestatus_filter:
291                 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
292                 if (nodestatus_data != self.nodestatus_filter):
293                     return False
294         return True
295
296 class SliceWidget(QWidget):
297     def __init__(self, parent):
298         QWidget.__init__(self, parent)
299
300         self.network_names = []
301         self.process = SfiProcess(self)
302
303         self.slicename = QLabel("", self)
304         self.updateSliceName()
305         self.slicename.setScaledContents(False)
306         filterlabel = QLabel ("Filter: ", self)
307         filterbox = QComboBox(self)
308         filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
309         searchlabel = QLabel ("Search: ", self)
310         searchlabel.setScaledContents(False)
311         searchbox = QLineEdit(self)
312         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
313
314         toplayout = QHBoxLayout()
315         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
316         toplayout.addStretch()
317         toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
318         toplayout.addWidget(filterbox, 0, Qt.AlignRight)
319         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
320         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
321
322         self.nodeView = NodeView(self)
323         self.nodeModel = QStandardItemModel(0, 4, self)
324         self.filterModel = NodeFilterProxyModel(self)
325
326         self.nodeNameDelegate = NodeNameDelegate(self)
327         self.nodeStatusDelegate = NodeStatusDelegate(self)
328
329         refresh = QPushButton("Refresh Slice Data", self)
330         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
331         renew = QPushButton("Renew Slice", self)
332         renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
333         submit = QPushButton("Submit", self)
334         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
335
336         bottomlayout = QHBoxLayout()
337         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
338         bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
339         bottomlayout.addStretch()
340         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
341
342         layout = QVBoxLayout()
343         layout.addLayout(toplayout)
344         layout.addWidget(self.nodeView)
345         layout.addLayout(bottomlayout)
346         self.setLayout(layout)
347         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
348
349         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
350         self.connect(renew, SIGNAL('clicked()'), self.renew)
351         self.connect(submit, SIGNAL('clicked()'), self.submit)
352         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
353         self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
354         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
355                      self.nodeSelectionChanged)
356
357         self.updateView()
358
359     def submitFinished(self):
360         faultString = self.process.getFaultString()
361         if not faultString:
362             self.setStatus("<font color='green'>Slice data submitted.</font>")
363         else:
364             self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
365
366         # no need to do that anymore
367         # QTimer.singleShot(1000, self.refresh)
368         self.updateView()
369         self.parent().signalAll("rspecUpdated")
370
371     def refreshFinished(self):
372         self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
373         self.updateView()
374         self.parent().signalAll("rspecUpdated")
375
376     def readSliceRSpec(self):
377         rspec_file = config.getSliceRSpecFile()
378         if os.path.exists(rspec_file):
379             xml = open(rspec_file).read()
380             return parse_rspec(xml)
381         return None
382
383     def setStatus(self, msg, timeout=None):
384         self.parent().setStatus(msg, timeout)
385
386     def checkRunningProcess(self):
387         if self.process.isRunning():
388             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
389             return True
390         return False
391
392     def search(self, search_string):
393         self.filterModel.setHostNameFilter(str(search_string))
394
395     def filter(self, filter_string):
396         self.filterModel.setNodeStatusFilter(str(filter_string))
397
398     def itemStatus(self, item):
399         statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
400         return statusItem.data(Qt.DisplayRole).toString()
401
402     def itemText(self, item):
403         return item.data(Qt.DisplayRole).toString()
404
405     # Recursively walk the tree, making changes to the RSpec
406     def process_subtree(self, rspec, item, depth = 0):
407         change = False
408         model = self.nodeModel
409
410         if depth in [0, 1]:
411             pass
412         elif depth == 2: # Hostname
413             hostname = self.itemText(item)
414             testbed = self.itemText(item.parent())
415             status = self.itemStatus(item)
416             if status == node_status['add']:
417                 print "Add hostname: %s" % hostname
418                 rspec.add_slivers(str(hostname), testbed)
419                 change = True
420             elif status == node_status['remove']:
421                 print "Remove hostname: %s" % hostname
422                 rspec.remove_slivers(str(hostname), testbed)
423                 change = True
424         elif depth == 3: # Tag
425             tag, value = self.itemText(item).split(": ")
426             status = self.itemStatus(item)
427             tag = "%s" % tag     # Prevent weird error from lxml
428             value = "%s" % value # Prevent weird error from lxml
429             node = self.itemText(item.parent())
430             testbed = self.itemText(item.parent().parent())
431             if status == tag_status['add']:
432                 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
433                 if node.startsWith(default_tags):
434                     rspec.add_default_sliver_attribute(tag, value, testbed)
435                 else:
436                     rspec.add_sliver_attribute(node, tag, value, testbed)
437                 change = True
438             elif status == tag_status['remove']:
439                 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
440                 if node.startsWith(default_tags):
441                     rspec.remove_default_sliver_attribute(tag, value, testbed)
442                 else:
443                     rspec.remove_sliver_attribute(node, tag, value, testbed)
444                 change = True
445
446         children = item.rowCount()
447         for row in range(0, children):
448             status = self.process_subtree(rspec, item.child(row), depth + 1)
449             change = change or status
450
451         return change
452
453     def submit(self):
454         if self.checkRunningProcess():
455             return
456
457         rspec = self.readSliceRSpec()
458         change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
459
460         if not change:
461             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
462             return
463
464         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
465         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
466
467         self.process.applyRSpec(rspec)
468         self.setStatus("Sending slice data (RSpec). This will take some time...")
469
470     def renew(self):
471         dlg = RenewWindow(parent=self)
472         dlg.exec_()
473
474     def refresh(self):
475         if not config.getSlice():
476             self.setStatus("<font color='red'>Slice not set yet!</font>")
477             return
478
479         if self.process.isRunning():
480             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
481             return
482
483         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
484         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
485
486         self.process.retrieveRspec()
487         self.setStatus("Refreshing slice data. This will take some time...")
488
489     def updateView(self):
490         global already_in_nodes
491         already_in_nodes = []
492         self.network_names = []
493         self.nodeModel.clear()
494
495         rspec = self.readSliceRSpec()
496         if not rspec:
497             return None
498
499         rootItem = self.nodeModel.invisibleRootItem()
500         #networks = sorted(rspec.get_network_list())
501         networks = rspec.get_networks()
502         for network in networks:
503             self.network_names.append(network)
504
505             #all_nodes = rspec.get_node_list(network)
506             #sliver_nodes = rspec.get_sliver_list(network)
507             all_nodes = rspec.get_nodes(network)
508             sliver_nodes = rspec.get_nodes_with_slivers(network)
509             available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
510
511             networkItem = QStandardItem(QString(network))
512             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
513             rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
514
515             already_in_nodes += sliver_nodes
516
517             # Add default slice tags
518             nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
519             statusItem = QStandardItem(QString(""))
520             nodeStatus = QStandardItem(QString(""))
521             networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
522             attrs = rspec.get_default_sliver_attributes(network)
523             for (name, value) in attrs:
524                     tagstring = QString("%s: %s" % (name, value))
525                     tagItem = QStandardItem(tagstring)
526                     status = QStandardItem(QString(tag_status['in']))
527                     nodeStatus = QStandardItem(QString(""))
528                     nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
529
530             for node in sliver_nodes:
531                 nodeItem = QStandardItem(QString(node))
532                 statusItem = QStandardItem(QString(node_status['in']))
533                 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
534                 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
535
536                 attrs = rspec.get_sliver_attributes(node, network)
537                 for (name, value) in attrs:
538                     tagstring = QString("%s: %s" % (name, value))
539                     tagItem = QStandardItem(tagstring)
540                     statusItem = QStandardItem(QString(tag_status['in']))
541                     nodeStatus = QStandardItem(QString(""))
542                     nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
543
544             for node in available_nodes:
545                 nodeItem = QStandardItem(QString(node))
546                 statusItem = QStandardItem(QString(node_status['out']))
547                 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
548                 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
549
550         self.filterModel.setSourceModel(self.nodeModel)
551         self.filterModel.setDynamicSortFilter(True)
552
553         headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
554         self.nodeModel.setHorizontalHeaderLabels(headers)
555
556         self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
557         self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
558         self.nodeView.setModel(self.filterModel)
559         self.nodeView.hideColumn(KIND_COLUMN)
560         self.nodeView.expandAll()
561         self.nodeView.resizeColumnToContents(0)
562         self.nodeView.collapseAll()
563
564     def updateSliceName(self):
565         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
566
567     def nodeSelectionChanged(self, hostname):
568         self.parent().nodeSelectionChanged(hostname)
569
570 class MainScreen(SfaScreen):
571     def __init__(self, parent):
572         SfaScreen.__init__(self, parent)
573
574         slice = SliceWidget(self)
575         self.init(slice, "Nodes", "OneLab SFA crawler")
576
577     def rspecUpdated(self):
578         self.mainwin.rspecWindow.updateView()
579
580     def configurationChanged(self):
581         self.widget.updateSliceName()
582         self.widget.updateView()
583         self.mainwin.rspecWindow.updateView()
584
585     def nodeSelectionChanged(self, hostname):
586         self.mainwin.nodeSelectionChanged(hostname)