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