make renew a self-contained dialog
[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, 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         self.setStatus("<font color='green'>Slice data submitted.</font>")
361         QTimer.singleShot(1000, self.refresh)
362
363     def refreshFinished(self):
364         self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
365         self.updateView()
366         self.parent().signalAll("rspecUpdated")
367
368     def readSliceRSpec(self):
369         rspec_file = config.getSliceRSpecFile()
370         if os.path.exists(rspec_file):
371             xml = open(rspec_file).read()
372             return parse_rspec(xml)
373         return None
374
375     def setStatus(self, msg, timeout=None):
376         self.parent().setStatus(msg, timeout)
377
378     def checkRunningProcess(self):
379         if self.process.isRunning():
380             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
381             return True
382         return False
383
384     def search(self, search_string):
385         self.filterModel.setHostNameFilter(str(search_string))
386
387     def filter(self, filter_string):
388         self.filterModel.setNodeStatusFilter(str(filter_string))
389
390     def itemStatus(self, item):
391         statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
392         return statusItem.data(Qt.DisplayRole).toString()
393
394     def itemText(self, item):
395         return item.data(Qt.DisplayRole).toString()
396
397     # Recursively walk the tree, making changes to the RSpec
398     def process_subtree(self, rspec, item, depth = 0):
399         change = False
400         model = self.nodeModel
401
402         if depth in [0, 1]:
403             pass
404         elif depth == 2: # Hostname
405             hostname = self.itemText(item)
406             testbed = self.itemText(item.parent())
407             status = self.itemStatus(item)
408             if status == node_status['add']:
409                 print "Add hostname: %s" % hostname
410                 rspec.add_slivers(str(hostname), testbed)
411                 change = True
412             elif status == node_status['remove']:
413                 print "Remove hostname: %s" % hostname
414                 rspec.remove_slivers(str(hostname), testbed)
415                 change = True
416         elif depth == 3: # Tag
417             tag, value = self.itemText(item).split(": ")
418             status = self.itemStatus(item)
419             tag = "%s" % tag     # Prevent weird error from lxml
420             value = "%s" % value # Prevent weird error from lxml
421             node = self.itemText(item.parent())
422             testbed = self.itemText(item.parent().parent())
423             if status == tag_status['add']:
424                 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
425                 if node.startsWith(default_tags):
426                     rspec.add_default_sliver_attribute(tag, value, testbed)
427                 else:
428                     rspec.add_sliver_attribute(node, tag, value, testbed)
429                 change = True
430             elif status == tag_status['remove']:
431                 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
432                 if node.startsWith(default_tags):
433                     rspec.remove_default_sliver_attribute(tag, value, testbed)
434                 else:
435                     rspec.remove_sliver_attribute(node, tag, value, testbed)
436                 change = True
437
438         children = item.rowCount()
439         for row in range(0, children):
440             status = self.process_subtree(rspec, item.child(row), depth + 1)
441             change = change or status
442
443         return change
444
445     def submit(self):
446         if self.checkRunningProcess():
447             return
448
449         rspec = self.readSliceRSpec()
450         change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
451
452         if not change:
453             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
454             return
455
456         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
457         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
458
459         self.process.applyRSpec(rspec)
460         self.setStatus("Sending slice data (RSpec). This will take some time...")
461
462     def renew(self):
463         dlg = RenewWindow(parent=self)
464         dlg.exec_()
465
466     def refresh(self):
467         if not config.getSlice():
468             self.setStatus("<font color='red'>Slice not set yet!</font>")
469             return
470
471         if self.process.isRunning():
472             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
473             return
474
475         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
476         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
477
478         self.process.getRSpecFromSM()
479         self.setStatus("Refreshing slice data. This will take some time...")
480
481     def updateView(self):
482         global already_in_nodes
483         already_in_nodes = []
484         self.network_names = []
485         self.nodeModel.clear()
486
487         rspec = self.readSliceRSpec()
488         if not rspec:
489             return None
490
491         rootItem = self.nodeModel.invisibleRootItem()
492         #networks = sorted(rspec.get_network_list())
493         networks = rspec.get_networks()
494         for network in networks:
495             self.network_names.append(network)
496
497             #all_nodes = rspec.get_node_list(network)
498             #sliver_nodes = rspec.get_sliver_list(network)
499             all_nodes = rspec.get_nodes(network)
500             sliver_nodes = rspec.get_nodes_with_slivers(network)
501             available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
502
503             networkItem = QStandardItem(QString(network))
504             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
505             rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
506
507             already_in_nodes += sliver_nodes
508
509             # Add default slice tags
510             nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
511             statusItem = QStandardItem(QString(""))
512             nodeStatus = QStandardItem(QString(""))
513             networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
514             attrs = rspec.get_default_sliver_attributes(network)
515             for (name, value) in attrs:
516                     tagstring = QString("%s: %s" % (name, value))
517                     tagItem = QStandardItem(tagstring)
518                     status = QStandardItem(QString(tag_status['in']))
519                     nodeStatus = QStandardItem(QString(""))
520                     nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
521
522             for node in sliver_nodes:
523                 nodeItem = QStandardItem(QString(node))
524                 statusItem = QStandardItem(QString(node_status['in']))
525                 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
526                 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
527
528                 attrs = rspec.get_sliver_attributes(node, network)
529                 for (name, value) in attrs:
530                     tagstring = QString("%s: %s" % (name, value))
531                     tagItem = QStandardItem(tagstring)
532                     statusItem = QStandardItem(QString(tag_status['in']))
533                     nodeStatus = QStandardItem(QString(""))
534                     nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
535
536             for node in available_nodes:
537                 nodeItem = QStandardItem(QString(node))
538                 statusItem = QStandardItem(QString(node_status['out']))
539                 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
540                 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
541
542         self.filterModel.setSourceModel(self.nodeModel)
543         self.filterModel.setDynamicSortFilter(True)
544
545         headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
546         self.nodeModel.setHorizontalHeaderLabels(headers)
547
548         self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
549         self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
550         self.nodeView.setModel(self.filterModel)
551         self.nodeView.hideColumn(KIND_COLUMN)
552         self.nodeView.expandAll()
553         self.nodeView.resizeColumnToContents(0)
554         self.nodeView.collapseAll()
555
556     def updateSliceName(self):
557         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
558
559     def nodeSelectionChanged(self, hostname):
560         self.parent().nodeSelectionChanged(hostname)
561
562 class MainScreen(SfaScreen):
563     def __init__(self, parent):
564         SfaScreen.__init__(self, parent)
565
566         slice = SliceWidget(self)
567         self.init(slice, "Nodes", "OneLab SFA crawler")
568
569     def rspecUpdated(self):
570         self.mainwin.rspecWindow.updateView()
571
572     def configurationChanged(self):
573         self.widget.updateSliceName()
574         self.widget.updateView()
575         self.mainwin.rspecWindow.updateView()
576
577     def nodeSelectionChanged(self, hostname):
578         self.mainwin.nodeSelectionChanged(hostname)