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