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