3 from PyQt4.QtCore import *
4 from PyQt4.QtGui import *
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
14 node_status = { "in": "Already Selected",
15 "out": "Not Selected",
17 "remove": "To be Removed"}
19 tag_status = { "in": "Already Set",
22 "remove": "To be Removed"}
24 default_tags = "Default tags"
25 settable_tags = ['delegations', 'initscript']
28 if index.parent().parent().isValid():
34 class NodeView(QTreeView):
35 def __init__(self, parent):
36 QTreeView.__init__(self, parent)
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.")
47 def mouseDoubleClickEvent(self, event):
48 index = self.currentIndex()
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()
55 if itemType(node_index) == "tag":
56 data = node_index.data().toString()
57 tagname, value = data.split(": ")
58 if tagname not in settable_tags:
60 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
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']))
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']))
79 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
81 def mousePressEvent(self, event):
82 QTreeView.mousePressEvent(self, event)
83 if event.button() == Qt.LeftButton:
87 index = self.currentIndex()
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()
94 if itemType(node_index) == "node":
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)
101 value, ok = QInputDialog.getText(self, "Add tag",
102 "Value for tag '%s'" % tagname)
104 # Add a new row to the model for the tag
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])
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])
122 elif status_data in (node_status['out'], node_status['remove']):
123 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
126 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
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)
136 class NodeNameDelegate(QStyledItemDelegate):
137 def __init__(self, parent):
138 QStyledItemDelegate.__init__(self, parent)
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()
145 fm = QFontMetrics(option.font)
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)
154 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
156 path = QPainterPath()
157 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
160 painter.setRenderHint(QPainter.Antialiasing)
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))
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))
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))
179 painter.setPen(QColor.fromRgb(0, 0, 0))
180 painter.drawText(option.rect, 0, QString(data))
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))
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))
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))
199 painter.setPen(QColor.fromRgb(0, 0, 0))
200 painter.drawText(option.rect, 0, QString(data))
204 class SliceWidget(QWidget):
205 def __init__(self, parent):
206 QWidget.__init__(self, parent)
208 self.network_names = []
209 self.process = SfiProcess(self)
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)
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)
225 self.nodeView = NodeView(self)
226 self.nodeModel = QStandardItemModel(0, 2, self)
227 self.filterModel = QSortFilterProxyModel(self) # enable filtering
229 self.nodeNameDelegate = NodeNameDelegate(self)
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)
236 bottomlayout = QHBoxLayout()
237 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
238 bottomlayout.addStretch()
239 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
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)
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)
256 def submitFinished(self):
257 self.setStatus("<font color='green'>Slice data submitted.</font>")
258 QTimer.singleShot(1000, self.refresh)
260 def refreshFinished(self):
261 self.setStatus("<font color='green'>Slice data updated.</font>", timeout=5000)
263 self.parent().signalAll("rspecUpdated")
265 def readSliceRSpec(self):
266 rspec_file = config.getSliceRSpecFile()
267 if os.path.exists(rspec_file):
268 xml = open(rspec_file).read()
272 def setStatus(self, msg, timeout=None):
273 self.parent().setStatus(msg, timeout)
275 def checkRunningProcess(self):
276 if self.process.isRunning():
277 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
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)))
290 def itemStatus(self, item):
291 statusItem = item.parent().child(item.row(), 1)
292 return statusItem.data(Qt.DisplayRole).toString()
294 def itemText(self, item):
295 return item.data(Qt.DisplayRole).toString()
297 # Recursively walk the tree, making changes to the RSpec
298 def process_subtree(self, rspec, item, depth = 0):
300 model = self.nodeModel
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)
312 elif status == node_status['remove']:
313 print "Remove hostname: %s" % hostname
314 rspec.remove_slivers(hostname, testbed)
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)
328 rspec.add_sliver_attribute(node, tag, value, testbed)
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)
335 rspec.remove_sliver_attribute(node, tag, value, testbed)
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
346 if self.checkRunningProcess():
349 rspec = self.readSliceRSpec()
350 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
353 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
356 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
357 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
359 self.process.applyRSpec(rspec)
360 self.setStatus("Sending slice data (RSpec). This will take some time...")
364 if not config.getSlice():
365 self.setStatus("<font color='red'>Slice not set yet!</font>")
368 if self.process.isRunning():
369 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
372 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
373 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
375 self.process.getRSpecFromSM()
376 self.setStatus("Updating slice data. This will take some time...")
378 def updateView(self):
379 global already_in_nodes
380 already_in_nodes = []
381 self.network_names = []
382 self.nodeModel.clear()
384 rspec = self.readSliceRSpec()
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)
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 ]
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))])
404 already_in_nodes += sliver_nodes
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])
417 for node in sliver_nodes:
418 nodeItem = QStandardItem(QString(node))
419 statusItem = QStandardItem(QString(node_status['in']))
420 networkItem.appendRow([nodeItem, statusItem])
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])
429 for node in available_nodes:
430 nodeItem = QStandardItem(QString(node))
431 statusItem = QStandardItem(QString(node_status['out']))
432 networkItem.appendRow([nodeItem, statusItem])
434 self.filterModel.setSourceModel(self.nodeModel)
435 self.filterModel.setFilterKeyColumn(-1)
436 self.filterModel.setDynamicSortFilter(True)
438 headers = QStringList() << "Hostname or Tag" << "Status"
439 self.nodeModel.setHorizontalHeaderLabels(headers)
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()
447 def updateSliceName(self):
448 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
450 def nodeSelectionChanged(self, hostname):
451 self.parent().nodeSelectionChanged(hostname)
453 class MainScreen(SfaScreen):
454 def __init__(self, parent):
455 SfaScreen.__init__(self, parent)
457 slice = SliceWidget(self)
458 self.init(slice, "Main Window", "OneLab SFA crawler")
460 def rspecUpdated(self):
461 self.mainwin.rspecWindow.updateView()
463 def configurationChanged(self):
464 self.widget.updateSliceName()
465 self.widget.updateView()
466 self.mainwin.rspecWindow.updateView()
468 def nodeSelectionChanged(self, hostname):
469 self.mainwin.nodeSelectionChanged(hostname)