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