5e2d090d1e7743866796ea048f91f01d57b7fceb
[sface.git] / sface / screens / mainscreen.py
1
2 import os
3 from PyQt4.QtCore import *
4 from PyQt4.QtGui import *
5
6 from sfa.util.rspecHelper import RSpec
7 from sface.config import config
8 from sface.sfiprocess import SfiProcess
9 from sface.screens.sfascreen import SfaScreen
10
11 already_in_nodes = []
12
13 node_status = { "in": "Already Selected",
14                 "out": "Not Selected",
15                 "add": "To be Added",
16                 "remove": "To be Removed"}
17
18 tag_status = { "in": "Already Set",
19                 "out": "Not Set",
20                 "add": "To be Added",
21                 "remove": "To be Removed"}
22
23 default_tags = "Default tags"
24 settable_tags = ['delegations', 'initscript']
25
26 def itemType(index):
27     if index.parent().parent().isValid():
28         return "tag"
29     else:
30         return "node"
31
32
33 class NodeView(QTreeView):
34     def __init__(self, parent):
35         QTreeView.__init__(self, parent)
36
37         self.setAnimated(True)
38         self.setItemsExpandable(True)
39         self.setRootIsDecorated(True)
40         self.setAlternatingRowColors(True)
41 #        self.setSelectionMode(self.MultiSelection)
42         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
43         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
44         self.setToolTip("Double click on a row to change its status")
45
46     def mouseDoubleClickEvent(self, event):
47         index = self.currentIndex()
48         model = index.model()
49         status_index = model.index(index.row(), 2, index.parent())
50         status_data = status_index.data().toString()
51         node_index = model.index(index.row(), 1, index.parent())
52         node_data = node_index.data().toString()
53
54         if itemType(node_index) == "tag":
55             tagname, value = node_index.data().toStringList()
56             if tagname not in settable_tags:
57                 # Pop up error msg
58                 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
59                 return
60             if status_data == tag_status['in']:
61                 model.setData(status_index, QString(tag_status['remove']))
62             elif status_data == tag_status['add']:
63                 model.setData(status_index, QString(tag_status['out']))
64             elif status_data == tag_status['remove']:
65                 model.setData(status_index, QString(tag_status['in']))
66             else: model.setData(status_index, QString(node_status['out']))
67         else:
68             # This is a hostname
69             if status_data == node_status['in']:
70                 model.setData(status_index, QString(node_status['remove']))
71             elif status_data == node_status['out']:
72                 model.setData(status_index, QString(node_status['add']))
73             elif status_data in (node_status['add'], node_status['remove']):
74                 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
75                 else: model.setData(status_index, QString(node_status['out']))
76
77         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
78
79     def currentChanged(self, current, previous):
80         model = current.model()
81         node_index = model.index(current.row(), 1, current.parent())
82         node_data = node_index.data().toString()
83         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
84         
85                 
86
87 class NodeNameDelegate(QStyledItemDelegate):
88     def __init__(self, parent):
89         QStyledItemDelegate.__init__(self, parent)
90
91     def paint(self, painter, option, index):
92         model = index.model()
93         status_index = model.index(index.row(), 2, index.parent())
94         status_data = status_index.data().toString()
95
96         fm = QFontMetrics(option.font)
97         rect = option.rect
98
99         if itemType(index) == "node":
100             data = "%s" % index.data().toString()
101             #if status_data not in (node_status['in'], node_status['remove'], node_status['add']):
102             # default view
103             #    QStyledItemDelegate.paint(self, painter, option, index)
104             #    return
105
106             rect.setHeight(rect.height() - 2)
107             rect.setWidth(fm.width(QString(data)) + 6)
108             rect.setX(rect.x() + 5)
109             rect.setY(rect.y() - 1)
110             x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
111
112             path = QPainterPath()
113             path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
114
115             painter.save()
116             painter.setRenderHint(QPainter.Antialiasing)
117
118             if status_data == node_status['in']: # already in the slice
119                 painter.fillPath(path, QColor("cyan"))
120                 painter.setPen(QColor.fromRgb(0, 0, 0))
121                 painter.drawText(option.rect, 0, QString(data))
122
123             elif status_data == node_status['add']: # newly added to the slice
124                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
125                 painter.setPen(QColor.fromRgb(0, 0, 0))
126                 painter.drawText(option.rect, 0, QString(data))
127
128             elif status_data == node_status['remove']: # removed from the slice
129                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
130                 painter.setPen(QColor.fromRgb(0, 0, 0))
131                 painter.drawText(option.rect, 0, QString(data))
132
133             else:
134                 painter.setPen(QColor.fromRgb(0, 0, 0))
135                 painter.drawText(option.rect, 0, QString(data))
136
137         else:
138             indent = 16
139             tag = index.data().toStringList()
140             data = "%s: %s" % (tag[0], tag[1])
141             rect.setHeight(rect.height() - 2)
142             rect.setWidth(fm.width(QString(data)) + 6 + indent)
143             rect.setX(rect.x() + 4 + indent)
144             rect.setY(rect.y() - 1)
145
146             x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
147
148             path = QPainterPath()
149             path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
150
151             painter.save()
152             painter.setRenderHint(QPainter.Antialiasing)
153
154             if status_data == tag_status['in']: # already in the slice
155                 painter.fillPath(path, QColor("cyan"))
156                 painter.setPen(QColor.fromRgb(0, 0, 0))
157                 painter.drawText(option.rect, 0, QString(data))
158
159             elif status_data == tag_status['add']: # newly added to the slice
160                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
161                 painter.setPen(QColor.fromRgb(0, 0, 0))
162                 painter.drawText(option.rect, 0, QString(data))
163
164             elif status_data == tag_status['remove']: # removed from the slice
165                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
166                 painter.setPen(QColor.fromRgb(0, 0, 0))
167                 painter.drawText(option.rect, 0, QString(data))
168
169             else:
170                 painter.setPen(QColor.fromRgb(0, 0, 0))
171                 painter.drawText(option.rect, 0, QString(data))
172
173         painter.restore()
174
175
176 class TreeItem:
177     def __init__(self, data, parent=None):
178         self.parentItem = parent
179         self.itemData = data
180         self.childItems = []
181
182     def clear(self):
183         for child in self.childItems:
184             child.clear()
185             del child
186         del self.childItems
187         self.childItems = []
188
189     def allChildItems(self):
190         all = []
191         for c in self.childItems:
192             all.append(c)
193             if c.childItems:
194                 all.extend(c.allChildItems())
195         return all
196
197     def appendChild(self, child):
198         self.childItems.append(child)
199
200     def child(self, row):
201         return self.childItems[row]
202     
203     def childCount(self):
204         return len(self.childItems)
205
206     def childNumber(self):
207         if self.parentItem:
208             return self.parentItem.childItems.index(self)
209         return 0
210
211     def columnCount(self):
212         return len(self.itemData)
213
214     def data(self, column):
215         return self.itemData[column]
216
217     def insertChildren(self, position, count, columns):
218         if position < 0 or position > len(self.childItems):
219             return False
220         
221         for row in range(count):
222             data = self.data(columns)
223             item = TreeItem(data, self)
224             self.childItems.insert(position, item)
225
226         return True
227
228     def insertColumns(self, position, columns):
229         if position < 0 or position > len(self.itemData):
230             return False
231
232         for column in range(columns):
233             self.itemData.insert(position, QVariant())
234         
235         for child in self.childItems:
236             child.insertColumns(position, columns)
237         
238         return True
239
240     def setData(self, column, value):
241         if column < 0 or column >= len(self.itemData):
242             return False
243
244         self.itemData[column] = value
245         return True
246     
247     def parent(self):
248         return self.parentItem
249
250
251 class NodeModel(QAbstractItemModel):
252     def __init__(self, parent):
253         QAbstractItemModel.__init__(self, parent)
254         self.__initRoot()
255
256     def clear(self):
257         self.rootItem.clear()
258         self.__initRoot()
259
260     def __initRoot(self):
261         self.rootItem = TreeItem([QString("Testbed"), QString("Hostname"), QString("Status")])
262
263     def getItem(self, index):
264         if index.isValid():
265             item = index.internalPointer()
266             if isinstance(item, TreeItem):
267                 return item
268         return self.rootItem
269
270     def headerData(self, section, orientation, role):
271         if orientation == Qt.Horizontal and role in (Qt.DisplayRole, Qt.EditRole):
272             return self.rootItem.data(section)
273         return QVariant()
274
275     def index(self, row, column, parent):
276         if not self.hasIndex(row, column, parent):
277             return QModelIndex()
278
279         parentItem = self.getItem(parent)
280         childItem = parentItem.child(row)
281         if childItem:
282             return self.createIndex(row, column, childItem)
283         else:
284             return QModelIndex()
285
286     def insertColumns(self, position, columns, parent):
287         self.beginInsertColumns(parent, position, position + columns -1)
288         ret = self.rootItem.insertColumns(position, columns)
289         self.endInsertColumns()
290         return ret
291
292     def insertRows(self, position, rows, parent):
293         parentItem = self.getItem(parent)
294         self.beginInsertRows(parent, position, position + rows -1)
295         ret = parentItem.insertChildren(position, rows, self.rootItem.columnCount())
296         self.endInsertRows()
297         return ret
298
299     def parent(self, index):
300         if not index.isValid():
301             return QModelIndex()
302
303         childItem = self.getItem(index)
304         parentItem = childItem.parent()
305         if parentItem is self.rootItem:
306             return QModelIndex()
307
308         return self.createIndex(parentItem.childNumber(), 0, parentItem)
309
310     def rowCount(self, parent=QModelIndex()):
311         parentItem = self.getItem(parent)
312         return parentItem.childCount()
313
314     def columnCount(self, parent=None):
315         return self.rootItem.columnCount()
316
317     def data(self, index, role):
318         if not index.isValid():
319             return QVariant()
320
321         if role != Qt.DisplayRole and role != Qt.EditRole:
322             return QVariant()
323
324         item = self.getItem(index)
325         return item.data(index.column())
326
327     def nodestatus(self, index):
328         if not index.isValid():
329             return QVariant()
330
331         item = self.getItem(index)
332         return item.nodestatus(index.column())
333
334     def flags(self, index):
335         if not index.isValid():
336             return 0
337         return Qt.ItemIsEnabled | Qt.ItemIsSelectable# | Qt.ItemIsEditable
338
339     def setData(self, index, value, role):
340         if role != Qt.EditRole:
341             return False
342
343         item = self.getItem(index)
344         ret = item.setData(index.column(), value)
345         if ret:
346             self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
347         return ret
348
349
350 class SliceWidget(QWidget):
351     def __init__(self, parent):
352         QWidget.__init__(self, parent)
353
354         self.network_names = []
355         self.process = SfiProcess(self)
356
357         self.slicename = QLabel("", self)
358         self.updateSliceName()
359         self.slicename.setScaledContents(False)
360         searchlabel = QLabel ("Search: ", self)
361         searchlabel.setScaledContents(False)
362         searchbox = QLineEdit(self)
363         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
364
365         toplayout = QHBoxLayout()
366         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
367         toplayout.addStretch()
368         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
369         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
370
371         self.nodeView = NodeView(self)
372         self.nodeModel = NodeModel(self)
373         self.filterModel = QSortFilterProxyModel(self) # enable filtering
374
375         self.nodeNameDelegate = NodeNameDelegate(self)
376
377         refresh = QPushButton("Update Slice Data", self)
378         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
379         submit = QPushButton("Submit", self)
380         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
381
382         bottomlayout = QHBoxLayout()
383         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
384         bottomlayout.addStretch()
385         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
386
387         layout = QVBoxLayout()
388         layout.addLayout(toplayout)
389         layout.addWidget(self.nodeView)
390         layout.addLayout(bottomlayout)
391         self.setLayout(layout)
392         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
393
394         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
395         self.connect(submit, SIGNAL('clicked()'), self.submit)
396         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
397         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
398                      self.nodeSelectionChanged)
399
400         self.updateView()
401
402     def submitFinished(self):
403         self.setStatus("<font color='green'>Slice data submitted.</font>")
404         QTimer.singleShot(1000, self.refresh)
405
406     def refreshFinished(self):
407         self.setStatus("<font color='green'>Slice data updated.</font>", timeout=5000)
408         self.updateView()
409         self.parent().signalAll("rspecUpdated")
410
411     def readSliceRSpec(self):
412         rspec_file = config.getSliceRSpecFile()
413         if os.path.exists(rspec_file):
414             xml = open(rspec_file).read()
415             return RSpec(xml)
416         return None
417
418     def setStatus(self, msg, timeout=None):
419         self.parent().setStatus(msg, timeout)
420
421     def checkRunningProcess(self):
422         if self.process.isRunning():
423             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
424             return True
425         return False
426
427     def filter(self, filter_string):
428         # for hierarchical models QSortFilterProxyModel applies the
429         # sort recursively. if the parent doesn't match the criteria
430         # we won't be able to match the children. so we need to match
431         # parent (by matching the network_names)
432         networks = ["^%s$" % n for n in self.network_names]
433         filters = networks + [str(filter_string)]
434         self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
435
436     def levels_down(self, item):
437         if item == self.nodeModel.rootItem:
438             return 0
439         else:
440             return self.levels_down(item.parent()) + 1
441
442     def submit(self):
443         if self.checkRunningProcess():
444             return
445
446         rspec = self.readSliceRSpec()
447         
448         no_change = True
449         all_child = self.nodeModel.rootItem.allChildItems()
450         for c in all_child:
451             testbed, name, status = c.itemData
452
453             if isinstance(status, QVariant):
454                 status = status.toString()
455
456             treelevel = self.levels_down(c)
457
458             if treelevel == 2:
459                 testbed = c.parent().itemData[0]
460                 if status == node_status['add']:
461                     #print "Add hostname: %s" % name
462                     rspec.add_sliver(name, testbed)
463                     no_change = False
464                 elif status == node_status['remove']:
465                     #print "Remove hostname: %s" % name
466                     rspec.remove_sliver(name, testbed)
467                     no_change = False
468             elif treelevel == 3:
469                 # A tag entry
470                 tag, value = name
471                 tag = "%s" % tag # Prevent weird error from lxml
472                 node = c.parent().itemData[1]
473                 testbed = c.parent().parent().itemData[0]
474                 if status == tag_status['add']:
475                     #print "Add tag: %s/%s " % (tag, value)
476                     if node.startswith(default_tags):
477                         rspec.add_default_sliver.attribute(tag, value, testbed)
478                     else:
479                         rspec.add_sliver_attribute(tag, value, node, testbed)
480                     no_change = False
481                 elif status == node_status['remove']:
482                     #print "Remove tag: %s/%s" % (tag, value)
483                     if node.startsWith(default_tags):
484                         rspec.remove_default_sliver_attribute(tag, value, testbed)
485                     else:
486                         rspec.remove_sliver_attribute(node, tag, value, testbed)
487                     no_change = False
488
489         if no_change:
490             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
491             return
492
493         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
494         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
495
496         self.process.applyRSpec(rspec)
497         self.setStatus("Sending slice data (RSpec). This will take some time...")
498         
499
500     def refresh(self):
501         if not config.getSlice():
502             self.setStatus("<font color='red'>Slice not set yet!</font>")
503             return
504
505         if self.process.isRunning():
506             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
507             return
508
509         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
510         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
511
512         self.process.getRSpecFromSM()
513         self.setStatus("Updating slice data. This will take some time...")
514
515     def updateView(self):
516         global already_in_nodes
517         already_in_nodes = []
518         self.network_names = []
519         self.nodeModel.clear()
520         
521         rspec = self.readSliceRSpec()
522         if not rspec:
523             return None
524
525         networks = rspec.get_network_list()
526         for network in networks:
527             self.network_names.append(network)
528             data = [QString(network), QString(""), QString("")]
529             networkItem = TreeItem(data, self.nodeModel.rootItem)
530             all_nodes = rspec.get_node_list(network)
531             sliver_nodes = rspec.get_sliver_list(network)
532             available_nodes = filter(lambda x:x not in sliver_nodes, all_nodes)
533
534             already_in_nodes += sliver_nodes
535
536             # Add default slice tags
537             data = [QString(""), QString("%s for %s" % (default_tags, network)),
538                     QString("")]
539             nodeItem = TreeItem(data, networkItem)
540             networkItem.appendChild(nodeItem)
541             attrs = rspec.get_default_sliver_attributes(network)
542             for (name, value) in attrs:
543                     tagstring = QStringList([name, value])
544                     data = [QString(""), tagstring, QString(tag_status['in'])]
545                     tagItem = TreeItem(data, nodeItem)
546                     nodeItem.appendChild(tagItem)
547
548             for node in sliver_nodes:
549                 data = [QString(""), QString(node), QString(node_status['in'])]
550                 nodeItem = TreeItem(data, networkItem)
551                 networkItem.appendChild(nodeItem)
552
553                 attrs = rspec.get_sliver_attributes(node, network)
554                 for (name, value) in attrs:
555                     tagstring = QStringList([name, value])
556                     data = [QString(""), tagstring, QString(tag_status['in'])]
557                     tagItem = TreeItem(data, nodeItem)
558                     nodeItem.appendChild(tagItem)
559
560             for node in available_nodes:
561                 nodeItem = TreeItem([QString(""), QString(node), QString(node_status['out'])], networkItem)
562                 networkItem.appendChild(nodeItem)
563
564             self.nodeModel.rootItem.appendChild(networkItem)
565
566         self.filterModel.setSourceModel(self.nodeModel)
567         self.filterModel.setFilterKeyColumn(-1)
568         self.filterModel.setDynamicSortFilter(True)
569
570         self.nodeView.setItemDelegateForColumn(1, self.nodeNameDelegate)
571         self.nodeView.setModel(self.filterModel)
572         self.nodeView.expandAll()
573         self.nodeView.resizeColumnToContents(1)
574
575     def updateSliceName(self):
576         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
577
578     def nodeSelectionChanged(self, hostname):
579         self.parent().nodeSelectionChanged(hostname)
580
581 class MainScreen(SfaScreen):
582     def __init__(self, parent):
583         SfaScreen.__init__(self, parent)
584
585         slice = SliceWidget(self)
586         self.init(slice, "Main Window", "OneLab Federation GUI")
587
588     def rspecUpdated(self):
589         self.mainwin.rspecWindow.updateView()
590         
591     def configurationChanged(self):
592         self.widget.updateSliceName()
593         self.widget.updateView()
594         self.mainwin.rspecWindow.updateView()
595
596     def nodeSelectionChanged(self, hostname):
597         self.mainwin.nodeSelectionChanged(hostname)