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))
359 self.parent().signalAll("rspecUpdated")
361 def refreshResourcesFinished(self):
362 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
364 faultString = self.process.getFaultString()
366 self.setStatus("Refreshing slice RSpec.")
367 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
368 self.process.retrieveRspec()
370 self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
372 def refreshRSpecFinished(self):
373 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
375 faultString = self.process.getFaultString()
377 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
379 self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
382 self.parent().signalAll("rspecUpdated")
384 def setStatus(self, msg, timeout=None):
385 self.parent().setStatus(msg, timeout)
387 def checkRunningProcess(self):
388 if self.process.isRunning():
389 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
393 def search(self, search_string):
394 self.filterModel.setHostNameFilter(str(search_string))
396 def filter(self, filter_string):
397 self.filterModel.setNodeStatusFilter(str(filter_string))
399 def itemStatus(self, item):
400 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
401 return str(statusItem.data(Qt.DisplayRole).toString())
403 def itemText(self, item):
404 return str(item.data(Qt.DisplayRole).toString())
406 # Recursively walk the tree, making changes to the RSpec
407 def process_subtree(self, rspec, resources, item, depth = 0):
409 model = self.nodeModel
413 elif depth == 2: # Hostname
414 hostname = self.itemText(item)
415 testbed = self.itemText(item.parent())
416 status = self.itemStatus(item)
417 if status == node_status['add']:
418 print "Add hostname: %s" % hostname
420 resource_node = resources.get_node_element(hostname)
422 if resource_node==None:
423 print "Error: Failed to find %s in resources rspec" % hostname
425 rspec.merge_node(resource_node, testbed)
426 rspec.add_slivers(str(hostname), testbed)
428 elif status == node_status['remove']:
429 print "Remove hostname: %s" % hostname
430 rspec.remove_slivers(str(hostname), testbed)
432 elif depth == 3: # Tag
433 tag, value = self.itemText(item).split(": ")
434 status = self.itemStatus(item)
435 tag = "%s" % tag # Prevent weird error from lxml
436 value = "%s" % value # Prevent weird error from lxml
437 node = self.itemText(item.parent())
438 testbed = self.itemText(item.parent().parent())
439 if status == tag_status['add']:
440 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
441 if node.startsWith(default_tags):
442 rspec.add_default_sliver_attribute(tag, value, testbed)
444 rspec.add_sliver_attribute(node, tag, value, testbed)
446 elif status == tag_status['remove']:
447 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
448 if node.startsWith(default_tags):
449 rspec.remove_default_sliver_attribute(tag, value, testbed)
451 rspec.remove_sliver_attribute(node, tag, value, testbed)
454 children = item.rowCount()
455 for row in range(0, children):
456 status = self.process_subtree(rspec, resources, item.child(row), depth + 1)
457 change = change or status
462 if self.checkRunningProcess():
465 rspec = SfiData().getSliceRSpec()
466 resources = SfiData().getResourcesRSpec()
467 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
470 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
473 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
475 self.process.applyRSpec(rspec)
476 self.setStatus("Sending slice data (RSpec). This will take some time...")
479 dlg = RenewWindow(parent=self)
483 if not config.getSlice():
484 self.setStatus("<font color='red'>Slice not set yet!</font>")
487 if self.process.isRunning():
488 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
491 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
493 self.process.retrieveResources()
494 self.setStatus("Refreshing resources. This will take some time...")
496 def updateView(self):
497 global already_in_nodes
498 already_in_nodes = []
499 self.network_names = []
500 self.nodeModel.clear()
502 rspec = SfiData().getSliceRSpec()
506 resources = SfiData().getResourcesRSpec()
510 rootItem = self.nodeModel.invisibleRootItem()
511 networks = rspec.get_networks()
513 for network in resources.get_networks():
514 if not network in networks:
515 networks.append(network)
517 for network in networks:
518 self.network_names.append(network)
520 all_nodes = resources.get_nodes(network)
521 sliver_nodes = rspec.get_nodes_with_slivers(network)
523 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
525 networkItem = QStandardItem(QString(network))
526 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
527 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
529 already_in_nodes += sliver_nodes
531 # Add default slice tags
532 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
533 statusItem = QStandardItem(QString(""))
534 nodeStatus = QStandardItem(QString(""))
535 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
536 attrs = rspec.get_default_sliver_attributes(network)
537 for (name, value) in attrs:
538 tagstring = QString("%s: %s" % (name, value))
539 tagItem = QStandardItem(tagstring)
540 status = QStandardItem(QString(tag_status['in']))
541 nodeStatus = QStandardItem(QString(""))
542 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
544 for node in sliver_nodes:
545 nodeItem = QStandardItem(QString(node))
546 statusItem = QStandardItem(QString(node_status['in']))
547 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
548 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
550 attrs = rspec.get_sliver_attributes(node, network)
551 for (name, value) in attrs:
552 tagstring = QString("%s: %s" % (name, value))
553 tagItem = QStandardItem(tagstring)
554 statusItem = QStandardItem(QString(tag_status['in']))
555 nodeStatus = QStandardItem(QString(""))
556 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
558 for node in available_nodes:
559 nodeItem = QStandardItem(QString(node))
560 statusItem = QStandardItem(QString(node_status['out']))
561 nodeStatus = QStandardItem(QString(resources.get_node_element(node, network).attrib.get("boot_state","")))
562 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
564 self.filterModel.setSourceModel(self.nodeModel)
565 self.filterModel.setDynamicSortFilter(True)
567 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
568 self.nodeModel.setHorizontalHeaderLabels(headers)
570 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
571 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
572 self.nodeView.setModel(self.filterModel)
573 self.nodeView.hideColumn(KIND_COLUMN)
574 self.nodeView.expandAll()
575 self.nodeView.resizeColumnToContents(0)
576 self.nodeView.collapseAll()
578 def updateSliceName(self):
579 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
581 def nodeSelectionChanged(self, hostname):
582 self.parent().nodeSelectionChanged(hostname)
584 class MainScreen(SfaScreen):
585 def __init__(self, parent):
586 SfaScreen.__init__(self, parent)
588 slice = SliceWidget(self)
589 self.init(slice, "Nodes", "OneLab SFA crawler")
591 def rspecUpdated(self):
592 self.mainwin.rspecWindow.updateView()
594 def configurationChanged(self):
595 self.widget.updateSliceName()
596 self.widget.updateView()
597 self.mainwin.rspecWindow.updateView()
599 def nodeSelectionChanged(self, hostname):
600 self.mainwin.nodeSelectionChanged(hostname)