4 from PyQt4.QtCore import *
5 from PyQt4.QtGui import *
7 #from sfa.util.rspecHelper import RSpec
8 from sfa.rspecs.rspec_parser import parse_rspec
9 from sface.config import config
10 from sface.sfirenew import RenewWindow
11 from sface.sfiprocess import SfiProcess
12 from sface.screens.sfascreen import SfaScreen
13 from sface.sfidata import SfiData
17 node_status = { "in": "Already Selected",
18 "out": "Not Selected",
20 "remove": "To be Removed"}
22 tag_status = { "in": "Already Set",
25 "remove": "To be Removed"}
27 color_status = { "in": QColor.fromRgb(0, 250, 250),
28 "add": QColor.fromRgb(0, 250, 0),
29 "remove": QColor.fromRgb(250, 0, 0) }
31 default_tags = "Default tags"
32 settable_tags = ['delegations', 'initscript']
35 NODE_STATUS_COLUMN = 1
36 MEMBERSHIP_STATUS_COLUMN = 2
39 # maximum length of a name to display before clipping
43 if index.parent().parent().isValid():
49 class NodeView(QTreeView):
50 def __init__(self, parent):
51 QTreeView.__init__(self, parent)
53 self.setAnimated(True)
54 self.setItemsExpandable(True)
55 self.setRootIsDecorated(True)
56 self.setAlternatingRowColors(True)
57 # self.setSelectionMode(self.MultiSelection)
58 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
59 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
60 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
62 def keyPressEvent(self, event):
63 if (event.key() == Qt.Key_Space):
64 self.toggleSelection()
66 QTreeView.keyPressEvent(self, event)
68 def mouseDoubleClickEvent(self, event):
69 self.toggleSelection()
71 def toggleSelection(self):
72 index = self.currentIndex()
74 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
75 status_data = status_index.data().toString()
76 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
77 node_data = node_index.data().toString()
79 if itemType(node_index) == "tag":
80 data = node_index.data().toString()
81 tagname, value = data.split(": ")
82 if tagname not in settable_tags:
84 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
86 if status_data == tag_status['in']:
87 model.setData(status_index, QString(tag_status['remove']))
88 elif status_data == tag_status['add']:
89 model.setData(status_index, QString(tag_status['out']))
90 elif status_data == tag_status['remove']:
91 model.setData(status_index, QString(tag_status['in']))
92 else: model.setData(status_index, QString(node_status['out']))
95 if status_data == node_status['in']:
96 model.setData(status_index, QString(node_status['remove']))
97 elif status_data == node_status['out']:
98 model.setData(status_index, QString(node_status['add']))
99 elif status_data in (node_status['add'], node_status['remove']):
100 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
101 else: model.setData(status_index, QString(node_status['out']))
103 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
105 def mousePressEvent(self, event):
106 QTreeView.mousePressEvent(self, event)
107 if event.button() == Qt.LeftButton:
111 index = self.currentIndex()
112 model = index.model()
113 status_index = model.index(index.row(), 1, index.parent())
114 status_data = status_index.data().toString()
115 node_index = model.index(index.row(), 0, index.parent())
116 node_data = node_index.data().toString()
118 if itemType(node_index) == "node":
120 if status_data in (node_status['in'], node_status['add'], ""):
121 # Pop up a dialog box for adding a new attribute
122 tagname, ok = QInputDialog.getItem(self, "Add tag",
123 "Tag name:", settable_tags)
125 value, ok = QInputDialog.getText(self, "Add tag",
126 "Value for tag '%s'" % tagname)
128 # Add a new row to the model for the tag
130 # For testing with the QStandardItemModel
131 #nodeItem = model.itemFromIndex(index)
132 #tagstring = QString("%s: %s" % (tagname, value))
133 #tagItem = QStandardItem(tagstring)
134 #status = QStandardItem(QString(tag_status['add']))
135 #nodeItem.appendRow([tagItem, status])
137 # We're using the QSortFilterProxyModel here
138 src_index = model.mapToSource(index)
139 src_model = src_index.model()
140 nodeItem = src_model.itemFromIndex(src_index)
141 tagstring = QString("%s: %s" % (tagname, value))
142 tagItem = QStandardItem(tagstring)
143 status = QStandardItem(QString(tag_status['add']))
144 nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
146 elif status_data in (node_status['out'], node_status['remove']):
147 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
150 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
152 def currentChanged(self, current, previous):
153 model = current.model()
154 node_index = model.index(current.row(), 0, current.parent())
155 node_data = node_index.data().toString()
156 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
160 class NodeNameDelegate(QStyledItemDelegate):
161 def __init__(self, parent):
162 QStyledItemDelegate.__init__(self, parent)
164 def displayText(self, value, locale):
165 data = str(QStyledItemDelegate.displayText(self, value, locale))
166 if (len(data)>NAME_MAX_LEN):
167 data = data[:(NAME_MAX_LEN-3)] + "..."
170 def paint(self, painter, option, index):
171 model = index.model()
172 data = str(self.displayText(index.data(), QLocale()))
173 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
174 status_data = status_index.data().toString()
176 fm = QFontMetrics(option.font)
177 rect = QRect(option.rect)
179 rect.setHeight(rect.height() - 2)
180 rect.setWidth(fm.width(QString(data)) + 6)
181 rect.setX(rect.x() + 5)
182 rect.setY(rect.y() - 1)
184 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
186 path = QPainterPath()
187 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
190 painter.setRenderHint(QPainter.Antialiasing)
192 if option.state & QStyle.State_Selected:
193 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
195 if itemType(index) == "node":
196 for x in node_status.keys():
197 if (node_status[x] == status_data) and (x in color_status):
198 painter.fillPath(path, color_status[x])
200 painter.setPen(QColor.fromRgb(0, 0, 0))
201 painter.drawText(rect, 0, QString(data))
204 for x in tag_status.keys():
205 if (tag_status[x] == status_data) and (x in color_status):
206 painter.fillPath(path, color_status[x])
208 painter.setPen(QColor.fromRgb(0, 0, 0))
209 painter.drawText(rect, 0, QString(data))
213 class NodeStatusDelegate(QStyledItemDelegate):
214 def __init__(self, parent):
215 QStyledItemDelegate.__init__(self, parent)
217 def paint(self, painter, option, index):
218 model = index.model()
219 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
220 nodestatus_data = nodestatus_index.data().toString()
222 fm = QFontMetrics(option.font)
223 rect = QRect(option.rect)
225 data = index.data().toString()
226 rect.setHeight(rect.height() - 2)
227 rect.setWidth(fm.width(QString(data)) + 6)
228 rect.setX(rect.x() + 5)
229 rect.setY(rect.y() - 1)
231 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
233 path = QPainterPath()
234 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
237 painter.setRenderHint(QPainter.Antialiasing)
239 if option.state & QStyle.State_Selected:
240 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
242 if (nodestatus_data == ""):
243 painter.setPen(QColor.fromRgb(0, 0, 0))
244 painter.drawText(rect, 0, QString(data))
245 elif (nodestatus_data == "boot"):
246 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
247 painter.setPen(QColor.fromRgb(0, 0, 0))
248 painter.drawText(rect, 0, QString(data))
250 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
251 painter.setPen(QColor.fromRgb(0, 0, 0))
252 painter.drawText(rect, 0, QString(data))
256 class NodeFilterProxyModel(QSortFilterProxyModel):
257 def __init__(self, parent=None):
258 QSortFilterProxyModel.__init__(self, parent)
259 self.hostname_filter_regex = None
260 self.nodestatus_filter = None
262 def setHostNameFilter(self, hostname):
263 self.hostname_filter_regex = QRegExp(hostname)
264 self.invalidateFilter()
266 def setNodeStatusFilter(self, status):
267 if (status == "all"):
268 self.nodestatus_filter = None
270 self.nodestatus_filter = status
271 self.invalidateFilter()
273 def filterAcceptsRow(self, sourceRow, source_parent):
274 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
275 if (kind_data == "node"):
276 if self.hostname_filter_regex:
277 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
278 if (self.hostname_filter_regex.indexIn(name_data) < 0):
280 if self.nodestatus_filter:
281 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
282 if (nodestatus_data != self.nodestatus_filter):
286 class SliceWidget(QWidget):
287 def __init__(self, parent):
288 QWidget.__init__(self, parent)
290 self.network_names = []
291 self.process = SfiProcess(self)
293 self.slicename = QLabel("", self)
294 self.updateSliceName()
295 self.slicename.setScaledContents(False)
296 filterlabel = QLabel ("Filter: ", self)
297 filterbox = QComboBox(self)
298 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
299 searchlabel = QLabel ("Search: ", self)
300 searchlabel.setScaledContents(False)
301 searchbox = QLineEdit(self)
302 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
304 toplayout = QHBoxLayout()
305 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
306 toplayout.addStretch()
307 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
308 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
309 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
310 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
312 self.nodeView = NodeView(self)
313 self.nodeModel = QStandardItemModel(0, 4, self)
314 self.filterModel = NodeFilterProxyModel(self)
316 self.nodeNameDelegate = NodeNameDelegate(self)
317 self.nodeStatusDelegate = NodeStatusDelegate(self)
319 refresh = QPushButton("Refresh Slice Data", self)
320 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
321 renew = QPushButton("Renew Slice", self)
322 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
323 submit = QPushButton("Submit", self)
324 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
326 bottomlayout = QHBoxLayout()
327 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
328 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
329 bottomlayout.addStretch()
330 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
332 layout = QVBoxLayout()
333 layout.addLayout(toplayout)
334 layout.addWidget(self.nodeView)
335 layout.addLayout(bottomlayout)
336 self.setLayout(layout)
337 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
339 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
340 self.connect(renew, SIGNAL('clicked()'), self.renew)
341 self.connect(submit, SIGNAL('clicked()'), self.submit)
342 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
343 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
344 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
345 self.nodeSelectionChanged)
349 def submitFinished(self):
350 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
352 faultString = self.process.getFaultString()
354 self.setStatus("<font color='green'>Slice data submitted.</font>")
356 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
358 # no need to do that anymore
359 # QTimer.singleShot(1000, self.refresh)
361 self.parent().signalAll("rspecUpdated")
363 def refreshResourcesFinished(self):
364 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
366 self.setStatus("Refreshing slice RSpec.")
367 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
368 self.process.retrieveRspec()
370 def refreshRSpecFinished(self):
371 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
372 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
374 self.parent().signalAll("rspecUpdated")
376 def setStatus(self, msg, timeout=None):
377 self.parent().setStatus(msg, timeout)
379 def checkRunningProcess(self):
380 if self.process.isRunning():
381 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
385 def search(self, search_string):
386 self.filterModel.setHostNameFilter(str(search_string))
388 def filter(self, filter_string):
389 self.filterModel.setNodeStatusFilter(str(filter_string))
391 def itemStatus(self, item):
392 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
393 return str(statusItem.data(Qt.DisplayRole).toString())
395 def itemText(self, item):
396 return str(item.data(Qt.DisplayRole).toString())
398 # Recursively walk the tree, making changes to the RSpec
399 def process_subtree(self, rspec, resources, item, depth = 0):
401 model = self.nodeModel
405 elif depth == 2: # Hostname
406 hostname = self.itemText(item)
407 testbed = self.itemText(item.parent())
408 status = self.itemStatus(item)
409 if status == node_status['add']:
410 print "Add hostname: %s" % hostname
412 resource_node = resources.get_node_element(hostname)
414 if resource_node==None:
415 print "Error: Failed to find %s in resources rspec" % hostname
417 rspec.merge_node(resource_node, testbed)
418 rspec.add_slivers(str(hostname), testbed)
420 elif status == node_status['remove']:
421 print "Remove hostname: %s" % hostname
422 rspec.remove_slivers(str(hostname), testbed)
424 elif depth == 3: # Tag
425 tag, value = self.itemText(item).split(": ")
426 status = self.itemStatus(item)
427 tag = "%s" % tag # Prevent weird error from lxml
428 value = "%s" % value # Prevent weird error from lxml
429 node = self.itemText(item.parent())
430 testbed = self.itemText(item.parent().parent())
431 if status == tag_status['add']:
432 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
433 if node.startsWith(default_tags):
434 rspec.add_default_sliver_attribute(tag, value, testbed)
436 rspec.add_sliver_attribute(node, tag, value, testbed)
438 elif status == tag_status['remove']:
439 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
440 if node.startsWith(default_tags):
441 rspec.remove_default_sliver_attribute(tag, value, testbed)
443 rspec.remove_sliver_attribute(node, tag, value, testbed)
446 children = item.rowCount()
447 for row in range(0, children):
448 status = self.process_subtree(rspec, resources, item.child(row), depth + 1)
449 change = change or status
454 if self.checkRunningProcess():
457 rspec = SfiData().getSliceRSpec()
458 resources = SfiData().getResourcesRSpec()
459 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
462 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
465 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
467 self.process.applyRSpec(rspec)
468 self.setStatus("Sending slice data (RSpec). This will take some time...")
471 dlg = RenewWindow(parent=self)
475 if not config.getSlice():
476 self.setStatus("<font color='red'>Slice not set yet!</font>")
479 if self.process.isRunning():
480 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
483 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
485 self.process.retrieveResources()
486 self.setStatus("Refreshing resources. This will take some time...")
488 def updateView(self):
489 global already_in_nodes
490 already_in_nodes = []
491 self.network_names = []
492 self.nodeModel.clear()
494 rspec = SfiData().getSliceRSpec()
498 resources = SfiData().getResourcesRSpec()
502 rootItem = self.nodeModel.invisibleRootItem()
503 networks = rspec.get_networks()
505 for network in resources.get_networks():
506 if not network in networks:
507 networks.append(network)
509 for network in networks:
510 self.network_names.append(network)
512 all_nodes = resources.get_nodes(network)
513 sliver_nodes = rspec.get_nodes_with_slivers(network)
515 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
517 networkItem = QStandardItem(QString(network))
518 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
519 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
521 already_in_nodes += sliver_nodes
523 # Add default slice tags
524 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
525 statusItem = QStandardItem(QString(""))
526 nodeStatus = QStandardItem(QString(""))
527 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
528 attrs = rspec.get_default_sliver_attributes(network)
529 for (name, value) in attrs:
530 tagstring = QString("%s: %s" % (name, value))
531 tagItem = QStandardItem(tagstring)
532 status = QStandardItem(QString(tag_status['in']))
533 nodeStatus = QStandardItem(QString(""))
534 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
536 for node in sliver_nodes:
537 nodeItem = QStandardItem(QString(node))
538 statusItem = QStandardItem(QString(node_status['in']))
539 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
540 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
542 attrs = rspec.get_sliver_attributes(node, network)
543 for (name, value) in attrs:
544 tagstring = QString("%s: %s" % (name, value))
545 tagItem = QStandardItem(tagstring)
546 statusItem = QStandardItem(QString(tag_status['in']))
547 nodeStatus = QStandardItem(QString(""))
548 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
550 for node in available_nodes:
551 nodeItem = QStandardItem(QString(node))
552 statusItem = QStandardItem(QString(node_status['out']))
553 nodeStatus = QStandardItem(QString(resources.get_node_element(node, network).attrib.get("boot_state","")))
554 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
556 self.filterModel.setSourceModel(self.nodeModel)
557 self.filterModel.setDynamicSortFilter(True)
559 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
560 self.nodeModel.setHorizontalHeaderLabels(headers)
562 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
563 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
564 self.nodeView.setModel(self.filterModel)
565 self.nodeView.hideColumn(KIND_COLUMN)
566 self.nodeView.expandAll()
567 self.nodeView.resizeColumnToContents(0)
568 self.nodeView.collapseAll()
570 def updateSliceName(self):
571 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
573 def nodeSelectionChanged(self, hostname):
574 self.parent().nodeSelectionChanged(hostname)
576 class MainScreen(SfaScreen):
577 def __init__(self, parent):
578 SfaScreen.__init__(self, parent)
580 slice = SliceWidget(self)
581 self.init(slice, "Nodes", "OneLab SFA crawler")
583 def rspecUpdated(self):
584 self.mainwin.rspecWindow.updateView()
586 def configurationChanged(self):
587 self.widget.updateSliceName()
588 self.widget.updateView()
589 self.mainwin.rspecWindow.updateView()
591 def nodeSelectionChanged(self, hostname):
592 self.mainwin.nodeSelectionChanged(hostname)