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