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