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 default_tags = "Default tags"
28 settable_tags = ['delegations', 'initscript']
31 NODE_STATUS_COLUMN = 1
32 MEMBERSHIP_STATUS_COLUMN = 2
35 # maximum length of a name to display before clipping
39 if index.parent().parent().isValid():
45 class NodeView(QTreeView):
46 def __init__(self, parent):
47 QTreeView.__init__(self, parent)
49 self.setAnimated(True)
50 self.setItemsExpandable(True)
51 self.setRootIsDecorated(True)
52 self.setAlternatingRowColors(True)
53 # self.setSelectionMode(self.MultiSelection)
54 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
55 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
56 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
58 def keyPressEvent(self, event):
59 if (event.key() == Qt.Key_Space):
60 self.toggleSelection()
62 QTreeView.keyPressEvent(self, event)
64 def mouseDoubleClickEvent(self, event):
65 self.toggleSelection()
67 def toggleSelection(self):
68 index = self.currentIndex()
70 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
71 status_data = status_index.data().toString()
72 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
73 node_data = node_index.data().toString()
75 if itemType(node_index) == "tag":
76 data = node_index.data().toString()
77 tagname, value = data.split(": ")
78 if tagname not in settable_tags:
80 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
82 if status_data == tag_status['in']:
83 model.setData(status_index, QString(tag_status['remove']))
84 elif status_data == tag_status['add']:
85 model.setData(status_index, QString(tag_status['out']))
86 elif status_data == tag_status['remove']:
87 model.setData(status_index, QString(tag_status['in']))
88 else: model.setData(status_index, QString(node_status['out']))
91 if status_data == node_status['in']:
92 model.setData(status_index, QString(node_status['remove']))
93 elif status_data == node_status['out']:
94 model.setData(status_index, QString(node_status['add']))
95 elif status_data in (node_status['add'], node_status['remove']):
96 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
97 else: model.setData(status_index, QString(node_status['out']))
99 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
101 def mousePressEvent(self, event):
102 QTreeView.mousePressEvent(self, event)
103 if event.button() == Qt.LeftButton:
107 index = self.currentIndex()
108 model = index.model()
109 status_index = model.index(index.row(), 1, index.parent())
110 status_data = status_index.data().toString()
111 node_index = model.index(index.row(), 0, index.parent())
112 node_data = node_index.data().toString()
114 if itemType(node_index) == "node":
116 if status_data in (node_status['in'], node_status['add'], ""):
117 # Pop up a dialog box for adding a new attribute
118 tagname, ok = QInputDialog.getItem(self, "Add tag",
119 "Tag name:", settable_tags)
121 value, ok = QInputDialog.getText(self, "Add tag",
122 "Value for tag '%s'" % tagname)
124 # Add a new row to the model for the tag
126 # For testing with the QStandardItemModel
127 #nodeItem = model.itemFromIndex(index)
128 #tagstring = QString("%s: %s" % (tagname, value))
129 #tagItem = QStandardItem(tagstring)
130 #status = QStandardItem(QString(tag_status['add']))
131 #nodeItem.appendRow([tagItem, status])
133 # We're using the QSortFilterProxyModel here
134 src_index = model.mapToSource(index)
135 src_model = src_index.model()
136 nodeItem = src_model.itemFromIndex(src_index)
137 tagstring = QString("%s: %s" % (tagname, value))
138 tagItem = QStandardItem(tagstring)
139 status = QStandardItem(QString(tag_status['add']))
140 nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
142 elif status_data in (node_status['out'], node_status['remove']):
143 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
146 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
148 def currentChanged(self, current, previous):
149 model = current.model()
150 node_index = model.index(current.row(), 0, current.parent())
151 node_data = node_index.data().toString()
152 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
156 class NodeNameDelegate(QStyledItemDelegate):
157 def __init__(self, parent):
158 QStyledItemDelegate.__init__(self, parent)
160 def displayText(self, value, locale):
161 data = str(QStyledItemDelegate.displayText(self, value, locale))
162 if (len(data)>NAME_MAX_LEN):
163 data = data[:(NAME_MAX_LEN-3)] + "..."
166 def paint(self, painter, option, index):
167 model = index.model()
168 data = str(self.displayText(index.data(), QLocale()))
169 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
170 status_data = status_index.data().toString()
172 fm = QFontMetrics(option.font)
173 rect = QRect(option.rect)
175 rect.setHeight(rect.height() - 2)
176 rect.setWidth(fm.width(QString(data)) + 6)
177 rect.setX(rect.x() + 5)
178 rect.setY(rect.y() - 1)
180 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
182 path = QPainterPath()
183 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
186 painter.setRenderHint(QPainter.Antialiasing)
188 if option.state & QStyle.State_Selected:
189 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
191 if itemType(index) == "node":
192 if status_data == node_status['in']: # already in the slice
193 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
194 painter.setPen(QColor.fromRgb(0, 0, 0))
195 painter.drawText(rect, 0, QString(data))
197 elif status_data == node_status['add']: # newly added to the slice
198 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
199 painter.setPen(QColor.fromRgb(0, 0, 0))
200 painter.drawText(rect, 0, QString(data))
202 elif status_data == node_status['remove']: # removed from the slice
203 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
204 painter.setPen(QColor.fromRgb(0, 0, 0))
205 painter.drawText(rect, 0, QString(data))
208 painter.setPen(QColor.fromRgb(0, 0, 0))
209 painter.drawText(rect, 0, QString(data))
212 if status_data == tag_status['in']: # already in the slice
213 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
214 painter.setPen(QColor.fromRgb(0, 0, 0))
215 painter.drawText(rect, 0, QString(data))
217 elif status_data == tag_status['add']: # newly added to the slice
218 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
219 painter.setPen(QColor.fromRgb(0, 0, 0))
220 painter.drawText(rect, 0, QString(data))
222 elif status_data == tag_status['remove']: # removed from the slice
223 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
224 painter.setPen(QColor.fromRgb(0, 0, 0))
225 painter.drawText(rect, 0, QString(data))
228 painter.setPen(QColor.fromRgb(0, 0, 0))
229 painter.drawText(rect, 0, QString(data))
233 class NodeStatusDelegate(QStyledItemDelegate):
234 def __init__(self, parent):
235 QStyledItemDelegate.__init__(self, parent)
237 def paint(self, painter, option, index):
238 model = index.model()
239 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
240 nodestatus_data = nodestatus_index.data().toString()
242 fm = QFontMetrics(option.font)
243 rect = QRect(option.rect)
245 data = index.data().toString()
246 rect.setHeight(rect.height() - 2)
247 rect.setWidth(fm.width(QString(data)) + 6)
248 rect.setX(rect.x() + 5)
249 rect.setY(rect.y() - 1)
251 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
253 path = QPainterPath()
254 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
257 painter.setRenderHint(QPainter.Antialiasing)
259 if option.state & QStyle.State_Selected:
260 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
262 if (nodestatus_data == ""):
263 painter.setPen(QColor.fromRgb(0, 0, 0))
264 painter.drawText(rect, 0, QString(data))
265 elif (nodestatus_data == "boot"):
266 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
267 painter.setPen(QColor.fromRgb(0, 0, 0))
268 painter.drawText(rect, 0, QString(data))
270 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
271 painter.setPen(QColor.fromRgb(0, 0, 0))
272 painter.drawText(rect, 0, QString(data))
276 class NodeFilterProxyModel(QSortFilterProxyModel):
277 def __init__(self, parent=None):
278 QSortFilterProxyModel.__init__(self, parent)
279 self.hostname_filter_regex = None
280 self.nodestatus_filter = None
282 def setHostNameFilter(self, hostname):
283 self.hostname_filter_regex = QRegExp(hostname)
284 self.invalidateFilter()
286 def setNodeStatusFilter(self, status):
287 if (status == "all"):
288 self.nodestatus_filter = None
290 self.nodestatus_filter = status
291 self.invalidateFilter()
293 def filterAcceptsRow(self, sourceRow, source_parent):
294 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
295 if (kind_data == "node"):
296 if self.hostname_filter_regex:
297 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
298 if (self.hostname_filter_regex.indexIn(name_data) < 0):
300 if self.nodestatus_filter:
301 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
302 if (nodestatus_data != self.nodestatus_filter):
306 class SliceWidget(QWidget):
307 def __init__(self, parent):
308 QWidget.__init__(self, parent)
310 self.network_names = []
311 self.process = SfiProcess(self)
313 self.slicename = QLabel("", self)
314 self.updateSliceName()
315 self.slicename.setScaledContents(False)
316 filterlabel = QLabel ("Filter: ", self)
317 filterbox = QComboBox(self)
318 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
319 searchlabel = QLabel ("Search: ", self)
320 searchlabel.setScaledContents(False)
321 searchbox = QLineEdit(self)
322 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
324 toplayout = QHBoxLayout()
325 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
326 toplayout.addStretch()
327 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
328 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
329 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
330 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
332 self.nodeView = NodeView(self)
333 self.nodeModel = QStandardItemModel(0, 4, self)
334 self.filterModel = NodeFilterProxyModel(self)
336 self.nodeNameDelegate = NodeNameDelegate(self)
337 self.nodeStatusDelegate = NodeStatusDelegate(self)
339 refresh = QPushButton("Refresh Slice Data", self)
340 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
341 renew = QPushButton("Renew Slice", self)
342 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
343 submit = QPushButton("Submit", self)
344 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
346 bottomlayout = QHBoxLayout()
347 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
348 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
349 bottomlayout.addStretch()
350 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
352 layout = QVBoxLayout()
353 layout.addLayout(toplayout)
354 layout.addWidget(self.nodeView)
355 layout.addLayout(bottomlayout)
356 self.setLayout(layout)
357 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
359 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
360 self.connect(renew, SIGNAL('clicked()'), self.renew)
361 self.connect(submit, SIGNAL('clicked()'), self.submit)
362 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
363 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
364 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
365 self.nodeSelectionChanged)
369 def submitFinished(self):
370 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
372 faultString = self.process.getFaultString()
374 self.setStatus("<font color='green'>Slice data submitted.</font>")
376 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
378 # no need to do that anymore
379 # QTimer.singleShot(1000, self.refresh)
381 self.parent().signalAll("rspecUpdated")
383 def refreshResourcesFinished(self):
384 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
386 self.setStatus("Refreshing slice RSpec.")
387 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
388 self.process.retrieveRspec()
390 def refreshRSpecFinished(self):
391 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
392 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
394 self.parent().signalAll("rspecUpdated")
396 def setStatus(self, msg, timeout=None):
397 self.parent().setStatus(msg, timeout)
399 def checkRunningProcess(self):
400 if self.process.isRunning():
401 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
405 def search(self, search_string):
406 self.filterModel.setHostNameFilter(str(search_string))
408 def filter(self, filter_string):
409 self.filterModel.setNodeStatusFilter(str(filter_string))
411 def itemStatus(self, item):
412 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
413 return str(statusItem.data(Qt.DisplayRole).toString())
415 def itemText(self, item):
416 return str(item.data(Qt.DisplayRole).toString())
418 # Recursively walk the tree, making changes to the RSpec
419 def process_subtree(self, rspec, resources, item, depth = 0):
421 model = self.nodeModel
425 elif depth == 2: # Hostname
426 hostname = self.itemText(item)
427 testbed = self.itemText(item.parent())
428 status = self.itemStatus(item)
429 if status == node_status['add']:
430 print "Add hostname: %s" % hostname
432 resource_node = resources.get_node_element(hostname)
434 if resource_node==None:
435 print "Error: Failed to find %s in resources rspec" % hostname
437 rspec.merge_node(resource_node, testbed)
438 rspec.add_slivers(str(hostname), testbed)
440 elif status == node_status['remove']:
441 print "Remove hostname: %s" % hostname
442 rspec.remove_slivers(str(hostname), testbed)
444 elif depth == 3: # Tag
445 tag, value = self.itemText(item).split(": ")
446 status = self.itemStatus(item)
447 tag = "%s" % tag # Prevent weird error from lxml
448 value = "%s" % value # Prevent weird error from lxml
449 node = self.itemText(item.parent())
450 testbed = self.itemText(item.parent().parent())
451 if status == tag_status['add']:
452 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
453 if node.startsWith(default_tags):
454 rspec.add_default_sliver_attribute(tag, value, testbed)
456 rspec.add_sliver_attribute(node, tag, value, testbed)
458 elif status == tag_status['remove']:
459 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
460 if node.startsWith(default_tags):
461 rspec.remove_default_sliver_attribute(tag, value, testbed)
463 rspec.remove_sliver_attribute(node, tag, value, testbed)
466 children = item.rowCount()
467 for row in range(0, children):
468 status = self.process_subtree(rspec, resources, item.child(row), depth + 1)
469 change = change or status
474 if self.checkRunningProcess():
477 rspec = SfiData().getSliceRSpec()
478 resources = SfiData().getResourcesRSpec()
479 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
482 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
485 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
487 self.process.applyRSpec(rspec)
488 self.setStatus("Sending slice data (RSpec). This will take some time...")
491 dlg = RenewWindow(parent=self)
495 if not config.getSlice():
496 self.setStatus("<font color='red'>Slice not set yet!</font>")
499 if self.process.isRunning():
500 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
503 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
505 self.process.retrieveResources()
506 self.setStatus("Refreshing resources. This will take some time...")
508 def updateView(self):
509 global already_in_nodes
510 already_in_nodes = []
511 self.network_names = []
512 self.nodeModel.clear()
514 rspec = SfiData().getSliceRSpec()
518 resources = SfiData().getResourcesRSpec()
522 rootItem = self.nodeModel.invisibleRootItem()
523 networks = rspec.get_networks()
525 for network in resources.get_networks():
526 if not network in networks:
527 networks.append(network)
529 for network in networks:
530 self.network_names.append(network)
532 all_nodes = resources.get_nodes(network)
533 sliver_nodes = rspec.get_nodes_with_slivers(network)
535 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
537 networkItem = QStandardItem(QString(network))
538 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
539 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
541 already_in_nodes += sliver_nodes
543 # Add default slice tags
544 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
545 statusItem = QStandardItem(QString(""))
546 nodeStatus = QStandardItem(QString(""))
547 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
548 attrs = rspec.get_default_sliver_attributes(network)
549 for (name, value) in attrs:
550 tagstring = QString("%s: %s" % (name, value))
551 tagItem = QStandardItem(tagstring)
552 status = QStandardItem(QString(tag_status['in']))
553 nodeStatus = QStandardItem(QString(""))
554 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
556 for node in sliver_nodes:
557 nodeItem = QStandardItem(QString(node))
558 statusItem = QStandardItem(QString(node_status['in']))
559 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
560 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
562 attrs = rspec.get_sliver_attributes(node, network)
563 for (name, value) in attrs:
564 tagstring = QString("%s: %s" % (name, value))
565 tagItem = QStandardItem(tagstring)
566 statusItem = QStandardItem(QString(tag_status['in']))
567 nodeStatus = QStandardItem(QString(""))
568 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
570 for node in available_nodes:
571 nodeItem = QStandardItem(QString(node))
572 statusItem = QStandardItem(QString(node_status['out']))
573 nodeStatus = QStandardItem(QString(resources.get_node_element(node, network).attrib.get("boot_state","")))
574 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
576 self.filterModel.setSourceModel(self.nodeModel)
577 self.filterModel.setDynamicSortFilter(True)
579 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
580 self.nodeModel.setHorizontalHeaderLabels(headers)
582 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
583 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
584 self.nodeView.setModel(self.filterModel)
585 self.nodeView.hideColumn(KIND_COLUMN)
586 self.nodeView.expandAll()
587 self.nodeView.resizeColumnToContents(0)
588 self.nodeView.collapseAll()
590 def updateSliceName(self):
591 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
593 def nodeSelectionChanged(self, hostname):
594 self.parent().nodeSelectionChanged(hostname)
596 class MainScreen(SfaScreen):
597 def __init__(self, parent):
598 SfaScreen.__init__(self, parent)
600 slice = SliceWidget(self)
601 self.init(slice, "Nodes", "OneLab SFA crawler")
603 def rspecUpdated(self):
604 self.mainwin.rspecWindow.updateView()
606 def configurationChanged(self):
607 self.widget.updateSliceName()
608 self.widget.updateView()
609 self.mainwin.rspecWindow.updateView()
611 def nodeSelectionChanged(self, hostname):
612 self.mainwin.nodeSelectionChanged(hostname)