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