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