5 from PyQt4.QtCore import *
6 from PyQt4.QtGui import *
8 #from sfa.util.rspecHelper import 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
15 from sface.clislicemgr import ClientSliceManager
19 node_status = { "in": "Already Selected",
20 "out": "Not Selected",
22 "remove": "To be Removed"}
24 tag_status = { "in": "Already Set",
27 "remove": "To be Removed"}
29 color_status = { "in": QColor.fromRgb(0, 250, 250),
30 "add": QColor.fromRgb(0, 250, 0),
31 "remove": QColor.fromRgb(250, 0, 0) }
33 default_tags = "Default tags"
34 settable_tags = ['delegations', 'initscript']
38 NODE_STATUS_COLUMN = 2
39 MEMBERSHIP_STATUS_COLUMN = 3
42 # maximum length of a name to display before clipping
46 if index.parent().parent().isValid():
52 class NodeView(QTreeView):
53 def __init__(self, parent):
54 QTreeView.__init__(self, parent)
56 self.setAnimated(True)
57 self.setItemsExpandable(True)
58 self.setRootIsDecorated(True)
59 self.setAlternatingRowColors(True)
60 # self.setSelectionMode(self.MultiSelection)
61 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
62 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
63 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
65 def keyPressEvent(self, event):
66 if (event.key() == Qt.Key_Space):
67 self.toggleSelection()
69 QTreeView.keyPressEvent(self, event)
71 def mouseDoubleClickEvent(self, event):
72 self.toggleSelection()
74 def toggleSelection(self):
75 index = self.currentIndex()
77 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
78 status_data = status_index.data().toString()
79 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
80 node_data = node_index.data().toString()
82 if itemType(node_index) == "tag":
83 data = node_index.data().toString()
84 tagname, value = data.split(": ")
85 if tagname not in settable_tags:
87 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
89 if status_data == tag_status['in']:
90 model.setData(status_index, QString(tag_status['remove']))
91 elif status_data == tag_status['add']:
92 model.setData(status_index, QString(tag_status['out']))
93 elif status_data == tag_status['remove']:
94 model.setData(status_index, QString(tag_status['in']))
95 else: model.setData(status_index, QString(node_status['out']))
98 if status_data == node_status['in']:
99 model.setData(status_index, QString(node_status['remove']))
100 elif status_data == node_status['out']:
101 model.setData(status_index, QString(node_status['add']))
102 elif status_data in (node_status['add'], node_status['remove']):
103 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
104 else: model.setData(status_index, QString(node_status['out']))
106 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
108 def appendRow(self, parent, name, nodeStatus="", nodeType="", membership="", kind = ""):
109 # row: name nodeStatus nodeType membership kind
110 item = QStandardItem(QString(str(name)))
112 QStandardItem(QString(str(nodeType))),
113 QStandardItem(QString(str(nodeStatus))),
114 QStandardItem(QString(str(membership))),
115 QStandardItem(QString(str(kind)))]
116 parent.appendRow(row)
119 def mousePressEvent(self, event):
120 QTreeView.mousePressEvent(self, event)
121 if event.button() == Qt.LeftButton:
125 index = self.currentIndex()
126 model = index.model()
127 status_index = model.index(index.row(), 1, index.parent())
128 status_data = status_index.data().toString()
129 node_index = model.index(index.row(), 0, index.parent())
130 node_data = node_index.data().toString()
132 if itemType(node_index) == "node":
134 if status_data in (node_status['in'], node_status['add'], ""):
135 # Pop up a dialog box for adding a new attribute
136 tagname, ok = QInputDialog.getItem(self, "Add tag",
137 "Tag name:", settable_tags)
139 value, ok = QInputDialog.getText(self, "Add tag",
140 "Value for tag '%s'" % tagname)
142 # We're using the QSortFilterProxyModel here
143 src_index = model.mapToSource(index)
144 src_model = src_index.model()
145 nodeItem = src_model.itemFromIndex(src_index)
147 self.appendRow(nodeItem, "%s: %s" % (tagname, value), membership=tag_status['add'], kind="attribute")
149 elif status_data in (node_status['out'], node_status['remove']):
150 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
153 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
155 def currentChanged(self, current, previous):
156 model = current.model()
157 node_index = model.index(current.row(), 0, current.parent())
158 node_data = node_index.data().toString()
159 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
163 class NodeNameDelegate(QStyledItemDelegate):
164 def __init__(self, parent):
165 QStyledItemDelegate.__init__(self, parent)
167 def displayText(self, value, locale):
168 data = str(QStyledItemDelegate.displayText(self, value, locale))
169 if (len(data)>NAME_MAX_LEN):
170 data = data[:(NAME_MAX_LEN-3)] + "..."
173 def paint(self, painter, option, index):
174 model = index.model()
175 data = str(self.displayText(index.data(), QLocale()))
176 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
177 status_data = status_index.data().toString()
179 fm = QFontMetrics(option.font)
180 rect = QRect(option.rect)
182 rect.setHeight(rect.height() - 2)
183 rect.setWidth(fm.width(QString(data)) + 6)
184 rect.setX(rect.x() + 5)
185 rect.setY(rect.y() - 1)
187 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
189 path = QPainterPath()
190 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
193 painter.setRenderHint(QPainter.Antialiasing)
195 if option.state & QStyle.State_Selected:
196 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
198 if itemType(index) == "node":
199 for x in node_status.keys():
200 if (node_status[x] == status_data) and (x in color_status):
201 painter.fillPath(path, color_status[x])
203 painter.setPen(QColor.fromRgb(0, 0, 0))
204 painter.drawText(rect, 0, QString(data))
207 for x in tag_status.keys():
208 if (tag_status[x] == status_data) and (x in color_status):
209 painter.fillPath(path, color_status[x])
211 painter.setPen(QColor.fromRgb(0, 0, 0))
212 painter.drawText(rect, 0, QString(data))
216 class NodeStatusDelegate(QStyledItemDelegate):
217 def __init__(self, parent):
218 QStyledItemDelegate.__init__(self, parent)
220 def paint(self, painter, option, index):
221 model = index.model()
222 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
223 nodestatus_data = nodestatus_index.data().toString()
225 fm = QFontMetrics(option.font)
226 rect = QRect(option.rect)
228 data = index.data().toString()
229 rect.setHeight(rect.height() - 2)
230 rect.setWidth(fm.width(QString(data)) + 6)
231 rect.setX(rect.x() + 5)
232 rect.setY(rect.y() - 1)
234 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
236 path = QPainterPath()
237 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
240 painter.setRenderHint(QPainter.Antialiasing)
242 if option.state & QStyle.State_Selected:
243 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
245 if (nodestatus_data == ""):
246 painter.setPen(QColor.fromRgb(0, 0, 0))
247 painter.drawText(rect, 0, QString(data))
248 elif (nodestatus_data == "boot"):
249 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
250 painter.setPen(QColor.fromRgb(0, 0, 0))
251 painter.drawText(rect, 0, QString(data))
253 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
254 painter.setPen(QColor.fromRgb(0, 0, 0))
255 painter.drawText(rect, 0, QString(data))
259 class NodeFilterProxyModel(QSortFilterProxyModel):
260 def __init__(self, parent=None):
261 QSortFilterProxyModel.__init__(self, parent)
262 self.hostname_filter_regex = None
263 self.nodestatus_filter = None
265 def setHostNameFilter(self, hostname):
266 self.hostname_filter_regex = QRegExp(hostname)
267 self.invalidateFilter()
269 def setNodeStatusFilter(self, status):
270 if (status == "all"):
271 self.nodestatus_filter = None
273 self.nodestatus_filter = status
274 self.invalidateFilter()
276 def filterAcceptsRow(self, sourceRow, source_parent):
277 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
278 if (kind_data == "node"):
279 if self.hostname_filter_regex:
280 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
281 if (self.hostname_filter_regex.indexIn(name_data) < 0):
283 if self.nodestatus_filter:
284 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
285 if (nodestatus_data != self.nodestatus_filter):
289 class SliceWidget(QWidget):
290 def __init__(self, parent):
291 QWidget.__init__(self, parent)
293 self.network_names = []
294 self.process = SfiProcess(self)
296 self.slicename = QLabel("", self)
297 self.updateSliceName()
298 self.slicename.setScaledContents(False)
299 filterlabel = QLabel ("Filter: ", self)
300 filterbox = QComboBox(self)
301 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
302 searchlabel = QLabel ("Search: ", self)
303 searchlabel.setScaledContents(False)
304 searchbox = QLineEdit(self)
305 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
307 toplayout = QHBoxLayout()
308 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
309 toplayout.addStretch()
310 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
311 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
312 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
313 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
315 self.nodeView = NodeView(self)
316 self.nodeModel = QStandardItemModel(0, 4, self)
317 self.filterModel = NodeFilterProxyModel(self)
319 self.nodeNameDelegate = NodeNameDelegate(self)
320 self.nodeStatusDelegate = NodeStatusDelegate(self)
322 refresh = QPushButton("Refresh Slice Data", self)
323 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
324 renew = QPushButton("Renew Slice", self)
325 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
326 submit = QPushButton("Submit", self)
327 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
329 bottomlayout = QHBoxLayout()
330 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
331 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
332 bottomlayout.addStretch()
333 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
335 layout = QVBoxLayout()
336 layout.addLayout(toplayout)
337 layout.addWidget(self.nodeView)
338 layout.addLayout(bottomlayout)
339 self.setLayout(layout)
340 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
342 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
343 self.connect(renew, SIGNAL('clicked()'), self.renew)
344 self.connect(submit, SIGNAL('clicked()'), self.submit_pg_compat)
345 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
346 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
347 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
348 self.nodeSelectionChanged)
352 def submitFinished(self):
353 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
355 faultString = self.process.getFaultString()
357 self.setStatus("<font color='green'>Slice data submitted.</font>")
359 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
362 self.parent().signalAll("rspecUpdated")
364 def refreshResourcesFinished(self):
365 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
367 faultString = self.process.getFaultString()
369 self.setStatus("Refreshing slice RSpec.")
370 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
371 self.process.retrieveRspec()
373 self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
375 def refreshRSpecFinished(self):
376 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
378 faultString = self.process.getFaultString()
380 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
382 self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
385 self.parent().signalAll("rspecUpdated")
387 def setStatus(self, msg, timeout=None):
388 self.parent().setStatus(msg, timeout)
390 def checkRunningProcess(self):
391 if self.process.isRunning():
392 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
396 def search(self, search_string):
397 self.filterModel.setHostNameFilter(str(search_string))
399 def filter(self, filter_string):
400 self.filterModel.setNodeStatusFilter(str(filter_string))
402 def itemStatus(self, item):
403 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
404 return str(statusItem.data(Qt.DisplayRole).toString())
406 def itemText(self, item):
407 return str(item.data(Qt.DisplayRole).toString())
409 # Recursively walk the tree, making changes to the RSpec
410 def process_subtree(self, rspec, resources, item, depth = 0):
412 model = self.nodeModel
416 elif depth == 2: # Hostname
417 hostname = self.itemText(item)
418 testbed = self.itemText(item.parent())
419 status = self.itemStatus(item)
420 if status == node_status['add']:
421 print "Add hostname: %s" % hostname
423 resource_node = resources.get_node_element(hostname)
425 if resource_node==None:
426 print "Error: Failed to find %s in resources rspec" % hostname
428 rspec.merge_node(resource_node, testbed)
429 rspec.add_slivers([{"hostname": str(hostname)}], testbed)
431 elif status == node_status['remove']:
432 print "Remove hostname: %s" % hostname
433 rspec.remove_slivers([{"hostname": str(hostname)}], testbed)
435 elif depth == 3: # Tag
436 tag, value = self.itemText(item).split(": ")
437 status = self.itemStatus(item)
438 tag = "%s" % tag # Prevent weird error from lxml
439 value = "%s" % value # Prevent weird error from lxml
440 node = self.itemText(item.parent())
441 testbed = self.itemText(item.parent().parent())
442 if status == tag_status['add']:
443 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
444 if node.startswith(default_tags):
445 rspec.add_default_sliver_attribute(tag, value, testbed)
447 rspec.add_sliver_attribute(node, tag, value, testbed)
449 elif status == tag_status['remove']:
450 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
451 if node.startswith(default_tags):
452 rspec.remove_default_sliver_attribute(tag, value, testbed)
454 rspec.remove_sliver_attribute(node, tag, value, testbed)
457 children = item.rowCount()
458 for row in range(0, children):
459 status = self.process_subtree(rspec, resources, item.child(row), depth + 1)
460 change = change or status
465 if self.checkRunningProcess():
468 rspec = SfiData().getSliceRSpec()
469 resources = SfiData().getResourcesRSpec()
470 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
473 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
476 # Several aggregates have issues with the <statistics> section in the
477 # rspec, so make sure it's not there.
478 stats_elems = rspec.xml.xpath("//statistics")
479 if len(stats_elems)>0:
480 stats_elem = stats_elems[0]
481 parent = stats_elem.xpath("..")[0]
482 parent.remove(stats_elem)
484 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
485 self.process.applyRSpec(rspec)
486 self.setStatus("Sending slice data (RSpec). This will take some time...")
488 def submit_pg_compat(self):
489 if self.checkRunningProcess():
492 rspec = SfiData().getSliceRSpec()
493 resources = SfiData().getResourcesRSpec()
494 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
497 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
500 dlg = ClientSliceManager(self)
501 dlg.submit_pg_compat(rspec)
504 self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
505 (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
506 QTimer.singleShot(2500, self.refresh)
509 dlg = RenewWindow(parent=self)
513 if not config.getSlice():
514 self.setStatus("<font color='red'>Slice not set yet!</font>")
517 if self.process.isRunning():
518 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
521 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
523 self.process.retrieveResources()
524 self.setStatus("Refreshing resources. This will take some time...")
526 def updateView(self):
527 global already_in_nodes
528 already_in_nodes = []
529 self.network_names = []
530 self.nodeModel.clear()
532 rspec = SfiData().getSliceRSpec()
536 resources = SfiData().getResourcesRSpec()
540 rootItem = self.nodeModel.invisibleRootItem()
541 networks = rspec.get_networks()
543 for network in resources.get_networks():
544 if not network in networks:
545 networks.append(network)
547 for network in networks:
548 self.network_names.append(network)
550 all_nodes = resources.get_nodes(network)
551 sliver_nodes = rspec.get_nodes_with_slivers(network)
553 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
555 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
556 networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
558 already_in_nodes += sliver_nodes
560 # Add default slice tags
561 self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
562 attrs = rspec.get_default_sliver_attributes(network)
563 for (name, value) in attrs:
564 tagstring = QString("%s: %s" % (name, value))
565 self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
567 for node in sliver_nodes:
568 self.nodeView.appendRow(networkItem,
570 nodeStatus=rspec.get_node_boot_state(node, network),
571 nodeType=rspec.get_node_sliver_type(node, network),
573 #get_node_element(node, network).attrib.get("boot_state",""),
574 #nodeType=rspec.get_node_element(node, network).attrib.get("sliver_type",""),
576 membership=node_status['in'],
579 attrs = rspec.get_sliver_attributes(node, network)
580 for (name, value) in attrs:
581 self.nodeView.appendRow(nodeItem,
582 "%s: %s" % (name, value),
583 membership=tag_status['in'],
586 for node in available_nodes:
587 self.nodeView.appendRow(networkItem,
589 #nodeStatus=resources.get_node_element(node, network).attrib.get("boot_state",""),
590 nodeStatus = resources.get_node_boot_state(node, network),
591 nodeType= resources.get_node_sliver_type(node, network),
592 membership=node_status['out'],
595 self.filterModel.setSourceModel(self.nodeModel)
596 self.filterModel.setDynamicSortFilter(True)
597 self.filterModel.sort(NAME_COLUMN)
599 headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
600 self.nodeModel.setHorizontalHeaderLabels(headers)
602 self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
603 self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
604 self.nodeView.setModel(self.filterModel)
605 self.nodeView.hideColumn(KIND_COLUMN)
606 self.nodeView.expandAll()
607 self.nodeView.resizeColumnToContents(0)
608 self.nodeView.collapseAll()
610 def updateSliceName(self):
611 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
613 def nodeSelectionChanged(self, hostname):
614 self.parent().nodeSelectionChanged(hostname)
616 class MainScreen(SfaScreen):
617 def __init__(self, parent):
618 SfaScreen.__init__(self, parent)
620 slice = SliceWidget(self)
621 self.init(slice, "Nodes", "OneLab SFA crawler")
623 def rspecUpdated(self):
624 self.mainwin.rspecWindow.updateView()
626 def configurationChanged(self):
627 self.widget.updateSliceName()
628 self.widget.updateView()
629 self.mainwin.rspecWindow.updateView()
631 def nodeSelectionChanged(self, hostname):
632 self.mainwin.nodeSelectionChanged(hostname)