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