5 from PyQt4.QtCore import *
6 from PyQt4.QtGui import *
8 #from sfa.util.rspecHelper import RSpec
9 from sfa.util.xrn import Xrn
10 from sface.config import config
11 from sface.sfirenew import RenewWindow
12 from sface.sfiprocess import SfiProcess
13 from sface.screens.sfascreen import SfaScreen
14 from sface.sfidata import SfiData
16 from sface.clislicemgr import ClientSliceManager
20 node_status = { "in": "Already Selected",
21 "out": "Not Selected",
23 "remove": "To be Removed"}
25 tag_status = { "in": "Already Set",
28 "remove": "To be Removed"}
30 color_status = { "in": QColor.fromRgb(0, 250, 250),
31 "add": QColor.fromRgb(0, 250, 0),
32 "remove": QColor.fromRgb(250, 0, 0) }
34 default_tags = "Default tags"
35 settable_tags = ['delegations', 'initscript']
39 NODE_STATUS_COLUMN = 2
40 MEMBERSHIP_STATUS_COLUMN = 3
43 # maximum length of a name to display before clipping
47 if index.parent().parent().isValid():
53 class NodeView(QTreeView):
54 def __init__(self, parent):
55 QTreeView.__init__(self, parent)
57 self.setAnimated(True)
58 self.setItemsExpandable(True)
59 self.setRootIsDecorated(True)
60 self.setAlternatingRowColors(True)
61 # self.setSelectionMode(self.MultiSelection)
62 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
63 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
64 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
66 def keyPressEvent(self, event):
67 if (event.key() == Qt.Key_Space):
68 self.toggleSelection()
70 QTreeView.keyPressEvent(self, event)
72 def mouseDoubleClickEvent(self, event):
73 self.toggleSelection()
75 def toggleSelection(self):
76 index = self.currentIndex()
78 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
79 status_data = status_index.data().toString()
80 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
81 node_data = node_index.data().toString()
83 if itemType(node_index) == "tag":
84 data = node_index.data().toString()
85 tagname, value = data.split(": ")
86 if tagname not in settable_tags:
88 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
90 if status_data == tag_status['in']:
91 model.setData(status_index, QString(tag_status['remove']))
92 elif status_data == tag_status['add']:
93 model.setData(status_index, QString(tag_status['out']))
94 elif status_data == tag_status['remove']:
95 model.setData(status_index, QString(tag_status['in']))
96 else: model.setData(status_index, QString(node_status['out']))
99 if status_data == node_status['in']:
100 model.setData(status_index, QString(node_status['remove']))
101 elif status_data == node_status['out']:
102 model.setData(status_index, QString(node_status['add']))
103 elif status_data in (node_status['add'], node_status['remove']):
104 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
105 else: model.setData(status_index, QString(node_status['out']))
107 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
109 def appendRow(self, parent, name, nodeStatus="", nodeType="", membership="", kind = ""):
110 # row: name nodeStatus nodeType membership kind
111 item = QStandardItem(QString(str(name)))
113 QStandardItem(QString(str(nodeType))),
114 QStandardItem(QString(str(nodeStatus))),
115 QStandardItem(QString(str(membership))),
116 QStandardItem(QString(str(kind)))]
117 parent.appendRow(row)
120 def mousePressEvent(self, event):
121 QTreeView.mousePressEvent(self, event)
122 if event.button() == Qt.LeftButton:
126 index = self.currentIndex()
127 model = index.model()
128 status_index = model.index(index.row(), 1, index.parent())
129 status_data = status_index.data().toString()
130 node_index = model.index(index.row(), 0, index.parent())
131 node_data = node_index.data().toString()
133 if itemType(node_index) == "node":
135 if status_data in (node_status['in'], node_status['add'], ""):
136 # Pop up a dialog box for adding a new attribute
137 tagname, ok = QInputDialog.getItem(self, "Add tag",
138 "Tag name:", settable_tags)
140 value, ok = QInputDialog.getText(self, "Add tag",
141 "Value for tag '%s'" % tagname)
143 # We're using the QSortFilterProxyModel here
144 src_index = model.mapToSource(index)
145 src_model = src_index.model()
146 nodeItem = src_model.itemFromIndex(src_index)
148 self.appendRow(nodeItem, "%s: %s" % (tagname, value), membership=tag_status['add'], kind="attribute")
150 elif status_data in (node_status['out'], node_status['remove']):
151 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
154 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
156 def currentChanged(self, current, previous):
157 model = current.model()
158 node_index = model.index(current.row(), 0, current.parent())
159 node_data = node_index.data().toString()
160 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
164 class NodeNameDelegate(QStyledItemDelegate):
165 def __init__(self, parent):
166 QStyledItemDelegate.__init__(self, parent)
168 def displayText(self, value, locale):
169 data = str(QStyledItemDelegate.displayText(self, value, locale))
170 if (len(data)>NAME_MAX_LEN):
171 data = data[:(NAME_MAX_LEN-3)] + "..."
174 def paint(self, painter, option, index):
175 model = index.model()
176 data = str(self.displayText(index.data(), QLocale()))
177 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
178 status_data = status_index.data().toString()
180 fm = QFontMetrics(option.font)
181 rect = QRect(option.rect)
183 rect.setHeight(rect.height() - 2)
184 rect.setWidth(fm.width(QString(data)) + 6)
185 rect.setX(rect.x() + 5)
186 rect.setY(rect.y() - 1)
188 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
190 path = QPainterPath()
191 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
194 painter.setRenderHint(QPainter.Antialiasing)
196 if option.state & QStyle.State_Selected:
197 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
199 if itemType(index) == "node":
200 for x in node_status.keys():
201 if (node_status[x] == status_data) and (x in color_status):
202 painter.fillPath(path, color_status[x])
204 painter.setPen(QColor.fromRgb(0, 0, 0))
205 painter.drawText(rect, 0, QString(data))
208 for x in tag_status.keys():
209 if (tag_status[x] == status_data) and (x in color_status):
210 painter.fillPath(path, color_status[x])
212 painter.setPen(QColor.fromRgb(0, 0, 0))
213 painter.drawText(rect, 0, QString(data))
217 class NodeStatusDelegate(QStyledItemDelegate):
218 def __init__(self, parent):
219 QStyledItemDelegate.__init__(self, parent)
221 def paint(self, painter, option, index):
222 model = index.model()
223 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
224 nodestatus_data = nodestatus_index.data().toString()
226 fm = QFontMetrics(option.font)
227 rect = QRect(option.rect)
229 data = index.data().toString()
230 rect.setHeight(rect.height() - 2)
231 rect.setWidth(fm.width(QString(data)) + 6)
232 rect.setX(rect.x() + 5)
233 rect.setY(rect.y() - 1)
235 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
237 path = QPainterPath()
238 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
241 painter.setRenderHint(QPainter.Antialiasing)
243 if option.state & QStyle.State_Selected:
244 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
246 if (nodestatus_data == ""):
247 painter.setPen(QColor.fromRgb(0, 0, 0))
248 painter.drawText(rect, 0, QString(data))
249 elif (nodestatus_data == "boot"):
250 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
251 painter.setPen(QColor.fromRgb(0, 0, 0))
252 painter.drawText(rect, 0, QString(data))
254 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
255 painter.setPen(QColor.fromRgb(0, 0, 0))
256 painter.drawText(rect, 0, QString(data))
260 class NodeFilterProxyModel(QSortFilterProxyModel):
261 def __init__(self, parent=None):
262 QSortFilterProxyModel.__init__(self, parent)
263 self.hostname_filter_regex = None
264 self.nodestatus_filter = None
266 def setHostNameFilter(self, hostname):
267 self.hostname_filter_regex = QRegExp(hostname)
268 self.invalidateFilter()
270 def setNodeStatusFilter(self, status):
271 if (status == "all"):
272 self.nodestatus_filter = None
274 self.nodestatus_filter = status
275 self.invalidateFilter()
277 def filterAcceptsRow(self, sourceRow, source_parent):
278 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
279 if (kind_data == "node"):
280 if self.hostname_filter_regex:
281 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
282 if (self.hostname_filter_regex.indexIn(name_data) < 0):
284 if self.nodestatus_filter:
285 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
286 if (nodestatus_data != self.nodestatus_filter):
290 class SliceWidget(QWidget):
291 def __init__(self, parent):
292 QWidget.__init__(self, parent)
294 self.network_names = []
295 self.process = SfiProcess(self)
297 self.slicename = QLabel("", self)
298 self.updateSliceName()
299 self.slicename.setScaledContents(False)
300 filterlabel = QLabel ("Filter: ", self)
301 filterbox = QComboBox(self)
302 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
303 searchlabel = QLabel ("Search: ", self)
304 searchlabel.setScaledContents(False)
305 searchbox = QLineEdit(self)
306 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
308 toplayout = QHBoxLayout()
309 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
310 toplayout.addStretch()
311 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
312 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
313 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
314 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
316 self.nodeView = NodeView(self)
317 self.nodeModel = QStandardItemModel(0, 4, self)
318 self.filterModel = NodeFilterProxyModel(self)
320 self.nodeNameDelegate = NodeNameDelegate(self)
321 self.nodeStatusDelegate = NodeStatusDelegate(self)
323 refresh = QPushButton("Refresh Slice Data", self)
324 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
325 renew = QPushButton("Renew Slice", self)
326 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
327 submit = QPushButton("Submit", self)
328 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
330 bottomlayout = QHBoxLayout()
331 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
332 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
333 bottomlayout.addStretch()
334 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
336 layout = QVBoxLayout()
337 layout.addLayout(toplayout)
338 layout.addWidget(self.nodeView)
339 layout.addLayout(bottomlayout)
340 self.setLayout(layout)
341 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
343 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
344 self.connect(renew, SIGNAL('clicked()'), self.renew)
345 self.connect(submit, SIGNAL('clicked()'), self.submit) # _pg_compat)
346 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
347 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
348 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
349 self.nodeSelectionChanged)
353 def submitFinished(self):
354 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
356 faultString = self.process.getFaultString()
358 self.setStatus("<font color='green'>Slice data submitted.</font>")
360 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
363 self.parent().signalAll("rspecUpdated")
365 def refreshResourcesFinished(self):
366 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
368 faultString = self.process.getFaultString()
370 self.setStatus("Refreshing slice RSpec.")
371 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
372 self.process.retrieveRspec()
374 self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
376 def refreshRSpecFinished(self):
377 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
379 faultString = self.process.getFaultString()
381 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
383 self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
386 self.parent().signalAll("rspecUpdated")
388 def setStatus(self, msg, timeout=None):
389 self.parent().setStatus(msg, timeout)
391 def checkRunningProcess(self):
392 if self.process.isRunning():
393 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
397 def search(self, search_string):
398 self.filterModel.setHostNameFilter(str(search_string))
400 def filter(self, filter_string):
401 self.filterModel.setNodeStatusFilter(str(filter_string))
403 def itemStatus(self, item):
404 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
405 return str(statusItem.data(Qt.DisplayRole).toString())
407 def itemText(self, item):
408 return str(item.data(Qt.DisplayRole).toString())
410 # Recursively walk the tree, making changes to the RSpec
411 def process_subtree(self, rspec, rspec_node_names, resource_node_names, item, depth = 0):
413 model = self.nodeModel
417 elif depth == 2: # Hostname
418 hostname = self.itemText(item)
419 testbed = self.itemText(item.parent())
420 status = self.itemStatus(item)
421 if status == node_status['add']:
422 print "Add hostname: %s" % hostname
424 resource_node = resource_node_names.get(hostname, None)
426 if resource_node==None:
427 print "Error: Failed to find %s in resources rspec" % hostname
429 if not (hostname in rspec_node_names):
430 network_name = Xrn(resource_node['component_manager_id']).get_hrn()
431 rspec.version.add_network(network_name)
432 rspec.version.add_nodes([resource_node])
433 rspec.version.add_slivers([str(hostname)])
435 elif status == node_status['remove']:
436 print "Remove hostname: %s" % hostname
437 rspec.version.remove_slivers([str(hostname)])
439 elif depth == 3: # Tag
440 tag, value = self.itemText(item).split(": ")
441 status = self.itemStatus(item)
442 tag = "%s" % tag # Prevent weird error from lxml
443 value = "%s" % value # Prevent weird error from lxml
444 node = self.itemText(item.parent())
445 testbed = self.itemText(item.parent().parent())
446 if status == tag_status['add']:
447 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
448 if node.startswith(default_tags):
449 rspec.add_default_sliver_attribute(tag, value, testbed)
451 rspec.add_sliver_attribute(node, tag, value, testbed)
453 elif status == tag_status['remove']:
454 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
455 if node.startswith(default_tags):
456 rspec.remove_default_sliver_attribute(tag, value, testbed)
458 rspec.remove_sliver_attribute(node, tag, value, testbed)
461 children = item.rowCount()
462 for row in range(0, children):
463 status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
464 change = change or status
469 if self.checkRunningProcess():
472 rspec = SfiData().getSliceRSpec()
473 resources = SfiData().getResourcesRSpec()
475 resource_node_names = self.nodesByName(resources.version.get_nodes())
476 rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
478 change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
481 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
484 # Several aggregates have issues with the <statistics> section in the
485 # rspec, so make sure it's not there.
486 stats_elems = rspec.xml.xpath("//statistics")
487 if len(stats_elems)>0:
488 stats_elem = stats_elems[0]
489 parent = stats_elem.xpath("..")[0]
490 parent.remove(stats_elem)
492 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
493 self.process.applyRSpec(rspec)
494 self.setStatus("Sending slice data (RSpec). This will take some time...")
496 def submit_pg_compat(self):
497 if self.checkRunningProcess():
500 rspec = SfiData().getSliceRSpec()
501 resources = SfiData().getResourcesRSpec()
502 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
505 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
508 dlg = ClientSliceManager(self)
509 dlg.submit_pg_compat(rspec)
512 self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
513 (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
514 QTimer.singleShot(2500, self.refresh)
517 dlg = RenewWindow(parent=self)
521 if not config.getSlice():
522 self.setStatus("<font color='red'>Slice not set yet!</font>")
525 if self.process.isRunning():
526 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
529 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
531 self.process.retrieveResources()
532 self.setStatus("Refreshing resources. This will take some time...")
534 def nodesByNetwork(self, nodeList):
536 for node in nodeList:
537 network_name = Xrn(node['component_manager_id']).get_hrn()
539 net = netDict.get(network_name, [])
541 netDict[network_name] = net
547 def nodesByName(self, nodeList, nameDict=None):
550 for node in nodeList:
551 hostname = node.get("component_name", None)
552 if hostname and (not hostname in nameDict):
553 nameDict[hostname] = node
557 def updateView(self):
558 global already_in_nodes
559 already_in_nodes = []
560 self.network_names = []
561 self.nodeModel.clear()
563 rspec = SfiData().getSliceRSpec()
567 resources = SfiData().getResourcesRSpec()
571 rootItem = self.nodeModel.invisibleRootItem()
574 for network in rspec.get_networks():
575 network_name = network.get("name", None)
576 if (network_name != None) and (not network_name in networks):
577 networks.append(network_name)
578 for network in resources.get_networks():
579 network_name = network.get("name", None)
580 if (network_name != None) and (not network_name in networks):
581 networks.append(network_name)
583 resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
584 rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
586 for network in networks:
587 self.network_names.append(network)
589 all_nodes = resources_nodes.get(network, [])
590 sliver_nodes = rspec_nodes.get(network, [])
592 sliver_node_names = self.nodesByName(sliver_nodes)
594 available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
596 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
597 networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
599 already_in_nodes += sliver_node_names.keys()
601 # Add default slice tags
602 nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
603 attrs = rspec.get_default_sliver_attributes(network)
604 for (name, value) in attrs:
605 tagstring = QString("%s: %s" % (name, value))
606 self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
608 for node in sliver_nodes:
609 self.nodeView.appendRow(networkItem,
610 node["component_name"],
611 nodeStatus=node.get("boot_state", ""),
612 #nodeType=node.get("rspec.get_node_sliver_type(node, network),
613 membership=node_status['in'],
616 attrs = rspec.get_sliver_attributes(node, network)
617 for (name, value) in attrs:
618 self.nodeView.appendRow(nodeItem,
619 "%s: %s" % (name, value),
620 membership=tag_status['in'],
623 for node in available_nodes:
624 self.nodeView.appendRow(networkItem,
625 node["component_name"],
626 nodeStatus = node.get("boot_state", ""),
627 #nodeType= resources.get_node_sliver_type(node, network),
628 membership=node_status['out'],
631 self.filterModel.setSourceModel(self.nodeModel)
632 self.filterModel.setDynamicSortFilter(True)
633 self.filterModel.sort(NAME_COLUMN)
635 headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
636 self.nodeModel.setHorizontalHeaderLabels(headers)
638 self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
639 self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
640 self.nodeView.setModel(self.filterModel)
641 self.nodeView.hideColumn(KIND_COLUMN)
642 self.nodeView.expandAll()
643 self.nodeView.resizeColumnToContents(0)
644 self.nodeView.collapseAll()
646 def updateSliceName(self):
647 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
649 def nodeSelectionChanged(self, hostname):
650 self.parent().nodeSelectionChanged(hostname)
652 class MainScreen(SfaScreen):
653 def __init__(self, parent):
654 SfaScreen.__init__(self, parent)
656 slice = SliceWidget(self)
657 self.init(slice, "Nodes", "OneLab SFA crawler")
659 def rspecUpdated(self):
660 self.mainwin.rspecWindow.updateView()
662 def configurationChanged(self):
663 self.widget.updateSliceName()
664 self.widget.updateView()
665 self.mainwin.rspecWindow.updateView()
667 def nodeSelectionChanged(self, hostname):
668 self.mainwin.nodeSelectionChanged(hostname)