use new rspec libs
[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 sfa.rspecs.rspec_parser import parse_rspec
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 tag_status = { "in": "Already Set",
20                 "out": "Not Set",
21                 "add": "To be Added",
22                 "remove": "To be Removed"}
23
24 default_tags = "Default tags"
25 settable_tags = ['delegations', 'initscript']
26
27 def itemType(index):
28     if index.parent().parent().isValid():
29         return "tag"
30     else:
31         return "node"
32
33
34 class NodeView(QTreeView):
35     def __init__(self, parent):
36         QTreeView.__init__(self, parent)
37
38         self.setAnimated(True)
39         self.setItemsExpandable(True)
40         self.setRootIsDecorated(True)
41         self.setAlternatingRowColors(True)
42 #        self.setSelectionMode(self.MultiSelection)
43         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
44         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
45         self.setToolTip("Double click on a row to change its status.  Right click on a host to add a tag.")
46
47     def mouseDoubleClickEvent(self, event):
48         index = self.currentIndex()
49         model = index.model()
50         status_index = model.index(index.row(), 1, index.parent())
51         status_data = status_index.data().toString()
52         node_index = model.index(index.row(), 0, index.parent())
53         node_data = node_index.data().toString()
54
55         if itemType(node_index) == "tag":
56             data = node_index.data().toString()
57             tagname, value = data.split(": ")
58             if tagname not in settable_tags:
59                 # Pop up error msg
60                 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
61                 return
62             if status_data == tag_status['in']:
63                 model.setData(status_index, QString(tag_status['remove']))
64             elif status_data == tag_status['add']:
65                 model.setData(status_index, QString(tag_status['out']))
66             elif status_data == tag_status['remove']:
67                 model.setData(status_index, QString(tag_status['in']))
68             else: model.setData(status_index, QString(node_status['out']))
69         else:
70             # This is a hostname
71             if status_data == node_status['in']:
72                 model.setData(status_index, QString(node_status['remove']))
73             elif status_data == node_status['out']:
74                 model.setData(status_index, QString(node_status['add']))
75             elif status_data in (node_status['add'], node_status['remove']):
76                 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
77                 else: model.setData(status_index, QString(node_status['out']))
78
79         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
80
81     def mousePressEvent(self, event):
82         QTreeView.mousePressEvent(self, event)
83         if event.button() == Qt.LeftButton:
84             return
85
86         # Right click
87         index = self.currentIndex()
88         model = index.model()
89         status_index = model.index(index.row(), 1, index.parent())
90         status_data = status_index.data().toString()
91         node_index = model.index(index.row(), 0, index.parent())
92         node_data = node_index.data().toString()
93
94         if itemType(node_index) == "node":
95             # This is a hostname
96             if status_data in (node_status['in'], node_status['add'], ""):
97                 # Pop up a dialog box for adding a new attribute
98                 tagname, ok = QInputDialog.getItem(self, "Add tag",
99                                                    "Tag name:", settable_tags)
100                 if ok:
101                     value, ok = QInputDialog.getText(self, "Add tag",
102                                                      "Value for tag '%s'" % tagname)
103                     if ok:
104                         # Add a new row to the model for the tag
105
106                         # For testing with the QStandardItemModel
107                         #nodeItem = model.itemFromIndex(index)
108                         #tagstring = QString("%s: %s" % (tagname, value))
109                         #tagItem = QStandardItem(tagstring)
110                         #status = QStandardItem(QString(tag_status['add']))
111                         #nodeItem.appendRow([tagItem, status])
112
113                         # We're using the QSortFilterProxyModel here
114                         src_index = model.mapToSource(index)
115                         src_model = src_index.model()
116                         nodeItem = src_model.itemFromIndex(src_index)
117                         tagstring = QString("%s: %s" % (tagname, value))
118                         tagItem = QStandardItem(tagstring)
119                         status = QStandardItem(QString(tag_status['add']))
120                         nodeItem.appendRow([tagItem, status])
121
122             elif status_data in (node_status['out'], node_status['remove']):
123                 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
124                 return
125
126         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
127
128     def currentChanged(self, current, previous):
129         model = current.model()
130         node_index = model.index(current.row(), 0, current.parent())
131         node_data = node_index.data().toString()
132         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
133         
134                 
135
136 class NodeNameDelegate(QStyledItemDelegate):
137     def __init__(self, parent):
138         QStyledItemDelegate.__init__(self, parent)
139
140     def paint(self, painter, option, index):
141         model = index.model()
142         status_index = model.index(index.row(), 1, index.parent())
143         status_data = status_index.data().toString()
144
145         fm = QFontMetrics(option.font)
146         rect = option.rect
147
148         data = index.data().toString()
149         rect.setHeight(rect.height() - 2)
150         rect.setWidth(fm.width(QString(data)) + 6)
151         rect.setX(rect.x() + 5)
152         rect.setY(rect.y() - 1)
153
154         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
155
156         path = QPainterPath()
157         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
158
159         painter.save()
160         painter.setRenderHint(QPainter.Antialiasing)
161
162         if itemType(index) == "node":
163             if status_data == node_status['in']: # already in the slice
164                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
165                 painter.setPen(QColor.fromRgb(0, 0, 0))
166                 painter.drawText(option.rect, 0, QString(data))
167
168             elif status_data == node_status['add']: # newly added to the slice
169                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
170                 painter.setPen(QColor.fromRgb(0, 0, 0))
171                 painter.drawText(option.rect, 0, QString(data))
172
173             elif status_data == node_status['remove']: # removed from the slice
174                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
175                 painter.setPen(QColor.fromRgb(0, 0, 0))
176                 painter.drawText(option.rect, 0, QString(data))
177
178             else:
179                 painter.setPen(QColor.fromRgb(0, 0, 0))
180                 painter.drawText(option.rect, 0, QString(data))
181
182         else:
183             if status_data == tag_status['in']: # already in the slice
184                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
185                 painter.setPen(QColor.fromRgb(0, 0, 0))
186                 painter.drawText(option.rect, 0, QString(data))
187
188             elif status_data == tag_status['add']: # newly added to the slice
189                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
190                 painter.setPen(QColor.fromRgb(0, 0, 0))
191                 painter.drawText(option.rect, 0, QString(data))
192
193             elif status_data == tag_status['remove']: # removed from the slice
194                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
195                 painter.setPen(QColor.fromRgb(0, 0, 0))
196                 painter.drawText(option.rect, 0, QString(data))
197
198             else:
199                 painter.setPen(QColor.fromRgb(0, 0, 0))
200                 painter.drawText(option.rect, 0, QString(data))
201
202         painter.restore()
203
204 class SliceWidget(QWidget):
205     def __init__(self, parent):
206         QWidget.__init__(self, parent)
207
208         self.network_names = []
209         self.process = SfiProcess(self)
210
211         self.slicename = QLabel("", self)
212         self.updateSliceName()
213         self.slicename.setScaledContents(False)
214         searchlabel = QLabel ("Search: ", self)
215         searchlabel.setScaledContents(False)
216         searchbox = QLineEdit(self)
217         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
218
219         toplayout = QHBoxLayout()
220         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
221         toplayout.addStretch()
222         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
223         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
224
225         self.nodeView = NodeView(self)
226         self.nodeModel = QStandardItemModel(0, 2, self)
227         self.filterModel = QSortFilterProxyModel(self) # enable filtering
228
229         self.nodeNameDelegate = NodeNameDelegate(self)
230
231         refresh = QPushButton("Update Slice Data", self)
232         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
233         submit = QPushButton("Submit", self)
234         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
235
236         bottomlayout = QHBoxLayout()
237         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
238         bottomlayout.addStretch()
239         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
240
241         layout = QVBoxLayout()
242         layout.addLayout(toplayout)
243         layout.addWidget(self.nodeView)
244         layout.addLayout(bottomlayout)
245         self.setLayout(layout)
246         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
247
248         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
249         self.connect(submit, SIGNAL('clicked()'), self.submit)
250         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
251         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
252                      self.nodeSelectionChanged)
253
254         self.updateView()
255
256     def submitFinished(self):
257         self.setStatus("<font color='green'>Slice data submitted.</font>")
258         QTimer.singleShot(1000, self.refresh)
259
260     def refreshFinished(self):
261         self.setStatus("<font color='green'>Slice data updated.</font>", timeout=5000)
262         self.updateView()
263         self.parent().signalAll("rspecUpdated")
264
265     def readSliceRSpec(self):
266         rspec_file = config.getSliceRSpecFile()
267         if os.path.exists(rspec_file):
268             xml = open(rspec_file).read()
269             return RSpec(xml)
270         return None
271
272     def setStatus(self, msg, timeout=None):
273         self.parent().setStatus(msg, timeout)
274
275     def checkRunningProcess(self):
276         if self.process.isRunning():
277             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
278             return True
279         return False
280
281     def filter(self, filter_string):
282         # for hierarchical models QSortFilterProxyModel applies the
283         # sort recursively. if the parent doesn't match the criteria
284         # we won't be able to match the children. so we need to match
285         # parent (by matching the network_names)
286         networks = ["^%s$" % n for n in self.network_names]
287         filters = networks + [str(filter_string)]
288         self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
289
290     def itemStatus(self, item):
291         statusItem = item.parent().child(item.row(), 1)
292         return statusItem.data(Qt.DisplayRole).toString()
293
294     def itemText(self, item):
295         return item.data(Qt.DisplayRole).toString()
296
297     # Recursively walk the tree, making changes to the RSpec
298     def process_subtree(self, rspec, item, depth = 0):
299         change = False
300         model = self.nodeModel
301
302         if depth in [0, 1]:
303             pass
304         elif depth == 2: # Hostname
305             hostname = self.itemText(item)
306             testbed = self.itemText(item.parent())
307             status = self.itemStatus(item)
308             if status == node_status['add']:
309                 print "Add hostname: %s" % hostname
310                 rspec.add_slivers(hostname, testbed)
311                 change = True
312             elif status == node_status['remove']:
313                 print "Remove hostname: %s" % hostname
314                 rspec.remove_slivers(hostname, testbed)
315                 change = True
316         elif depth == 3: # Tag
317             tag, value = self.itemText(item).split(": ")
318             status = self.itemStatus(item)
319             tag = "%s" % tag     # Prevent weird error from lxml
320             value = "%s" % value # Prevent weird error from lxml
321             node = self.itemText(item.parent())
322             testbed = self.itemText(item.parent().parent())
323             if status == tag_status['add']:
324                 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
325                 if node.startsWith(default_tags):
326                     rspec.add_default_sliver_attribute(tag, value, testbed)
327                 else:
328                     rspec.add_sliver_attribute(node, tag, value, testbed)
329                 change = True
330             elif status == tag_status['remove']:
331                 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
332                 if node.startsWith(default_tags):
333                     rspec.remove_default_sliver_attribute(tag, value, testbed)
334                 else:
335                     rspec.remove_sliver_attribute(node, tag, value, testbed)
336                 change = True
337
338         children = item.rowCount()
339         for row in range(0, children):
340             status = self.process_subtree(rspec, item.child(row), depth + 1)
341             change = change or status
342
343         return change
344
345     def submit(self):
346         if self.checkRunningProcess():
347             return
348
349         rspec = self.readSliceRSpec()
350         change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
351
352         if not change:
353             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
354             return
355
356         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
357         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
358
359         self.process.applyRSpec(rspec)
360         self.setStatus("Sending slice data (RSpec). This will take some time...")
361         
362
363     def refresh(self):
364         if not config.getSlice():
365             self.setStatus("<font color='red'>Slice not set yet!</font>")
366             return
367
368         if self.process.isRunning():
369             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
370             return
371
372         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
373         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
374
375         self.process.getRSpecFromSM()
376         self.setStatus("Updating slice data. This will take some time...")
377
378     def updateView(self):
379         global already_in_nodes
380         already_in_nodes = []
381         self.network_names = []
382         self.nodeModel.clear()
383
384         rspec = self.readSliceRSpec()
385         if not rspec:
386             return None
387
388         rootItem = self.nodeModel.invisibleRootItem()
389         #networks = sorted(rspec.get_network_list())
390         networks = rspec.get_networks()
391         for network in networks:
392             self.network_names.append(network)
393
394             #all_nodes = rspec.get_node_list(network)
395             #sliver_nodes = rspec.get_sliver_list(network)
396             all_nodes = rspec.get_nodes(network)
397             sliver_nodes = rspec.get_nodes_with_slivers(network)
398             available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
399
400             networkItem = QStandardItem(QString(network))
401             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
402             rootItem.appendRow([networkItem, QStandardItem(QString(msg))])
403
404             already_in_nodes += sliver_nodes
405
406             # Add default slice tags
407             nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
408             statusItem = QStandardItem(QString(""))
409             networkItem.appendRow([nodeItem, statusItem])
410             attrs = rspec.get_default_sliver_attributes(network)
411             for (name, value) in attrs:
412                     tagstring = QString("%s: %s" % (name, value))
413                     tagItem = QStandardItem(tagstring)
414                     status = QStandardItem(QString(tag_status['in']))
415                     nodeItem.appendRow([tagItem, status])
416
417             for node in sliver_nodes:
418                 nodeItem = QStandardItem(QString(node))
419                 statusItem = QStandardItem(QString(node_status['in']))
420                 networkItem.appendRow([nodeItem, statusItem])
421
422                 attrs = rspec.get_sliver_attributes(node, network)
423                 for (name, value) in attrs:
424                     tagstring = QString("%s: %s" % (name, value))
425                     tagItem = QStandardItem(tagstring)
426                     statusItem = QStandardItem(QString(tag_status['in']))
427                     nodeItem.appendRow([tagItem, statusItem])
428
429             for node in available_nodes:
430                 nodeItem = QStandardItem(QString(node))
431                 statusItem = QStandardItem(QString(node_status['out']))
432                 networkItem.appendRow([nodeItem, statusItem])
433
434         self.filterModel.setSourceModel(self.nodeModel)
435         self.filterModel.setFilterKeyColumn(-1)
436         self.filterModel.setDynamicSortFilter(True)
437
438         headers = QStringList() << "Hostname or Tag" << "Status"
439         self.nodeModel.setHorizontalHeaderLabels(headers)
440
441         self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
442         self.nodeView.setModel(self.filterModel)
443         self.nodeView.expandAll()
444         self.nodeView.resizeColumnToContents(0)
445         self.nodeView.collapseAll()
446
447     def updateSliceName(self):
448         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
449
450     def nodeSelectionChanged(self, hostname):
451         self.parent().nodeSelectionChanged(hostname)
452
453 class MainScreen(SfaScreen):
454     def __init__(self, parent):
455         SfaScreen.__init__(self, parent)
456
457         slice = SliceWidget(self)
458         self.init(slice, "Main Window", "OneLab SFA crawler")
459
460     def rspecUpdated(self):
461         self.mainwin.rspecWindow.updateView()
462         
463     def configurationChanged(self):
464         self.widget.updateSliceName()
465         self.widget.updateView()
466         self.mainwin.rspecWindow.updateView()
467
468     def nodeSelectionChanged(self, hostname):
469         self.mainwin.nodeSelectionChanged(hostname)