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