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