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