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 def lessThan(self, left, right):
291 l_str = str(left.data().toString())
292 r_str = str(right.data().toString())
294 # make sure default_tags appears before everything else
295 if l_str.startswith(default_tags):
298 if r_str.startswith(default_tags):
301 return (l_str < r_str)
304 class SliceWidget(QWidget):
305 def __init__(self, parent):
306 QWidget.__init__(self, parent)
308 self.network_names = []
309 self.process = SfiProcess(self)
311 self.slicename = QLabel("", self)
312 self.updateSliceName()
313 self.slicename.setScaledContents(False)
314 filterlabel = QLabel ("Filter: ", self)
315 filterbox = QComboBox(self)
316 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
317 searchlabel = QLabel ("Search: ", self)
318 searchlabel.setScaledContents(False)
319 searchbox = QLineEdit(self)
320 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
322 toplayout = QHBoxLayout()
323 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
324 toplayout.addStretch()
325 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
326 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
327 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
328 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
330 self.nodeView = NodeView(self)
331 self.nodeModel = QStandardItemModel(0, 4, self)
332 self.filterModel = NodeFilterProxyModel(self)
334 self.nodeNameDelegate = NodeNameDelegate(self)
335 self.nodeStatusDelegate = NodeStatusDelegate(self)
337 refresh = QPushButton("Refresh Slice Data", self)
338 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
339 renew = QPushButton("Renew Slice", self)
340 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
341 submit = QPushButton("Submit", self)
342 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
344 bottomlayout = QHBoxLayout()
345 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
346 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
347 bottomlayout.addStretch()
348 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
350 layout = QVBoxLayout()
351 layout.addLayout(toplayout)
352 layout.addWidget(self.nodeView)
353 layout.addLayout(bottomlayout)
354 self.setLayout(layout)
355 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
357 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
358 self.connect(renew, SIGNAL('clicked()'), self.renew)
359 self.connect(submit, SIGNAL('clicked()'), self.submit) # _pg_compat)
360 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
361 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
362 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
363 self.nodeSelectionChanged)
367 def submitFinished(self):
368 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
370 faultString = self.process.getFaultString()
372 self.setStatus("<font color='green'>Slice data submitted.</font>")
374 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
377 self.parent().signalAll("rspecUpdated")
379 def refreshResourcesFinished(self):
380 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
382 faultString = self.process.getFaultString()
384 self.setStatus("Refreshing slice RSpec.")
385 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
386 self.process.retrieveRspec()
388 self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
390 def refreshRSpecFinished(self):
391 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
393 faultString = self.process.getFaultString()
395 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
397 self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
400 self.parent().signalAll("rspecUpdated")
402 def setStatus(self, msg, timeout=None):
403 self.parent().setStatus(msg, timeout)
405 def checkRunningProcess(self):
406 if self.process.isRunning():
407 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
411 def search(self, search_string):
412 self.filterModel.setHostNameFilter(str(search_string))
414 def filter(self, filter_string):
415 self.filterModel.setNodeStatusFilter(str(filter_string))
417 def itemStatus(self, item):
418 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
419 return str(statusItem.data(Qt.DisplayRole).toString())
421 def itemText(self, item):
422 return str(item.data(Qt.DisplayRole).toString())
424 # Recursively walk the tree, making changes to the RSpec
425 def process_subtree(self, rspec, rspec_node_names, resource_node_names, item, depth = 0):
427 model = self.nodeModel
431 elif depth == 2: # Hostname
432 hostname = self.itemText(item)
433 testbed = self.itemText(item.parent())
434 status = self.itemStatus(item)
435 if status == node_status['add']:
436 print "Add hostname: %s" % hostname
438 resource_node = resource_node_names.get(hostname, None)
440 if resource_node==None:
441 print "Error: Failed to find %s in resources rspec" % hostname
443 if not (hostname in rspec_node_names):
444 network_name = Xrn(resource_node['component_manager_id']).get_hrn()
445 rspec.version.add_network(network_name)
446 rspec.version.add_nodes([resource_node])
447 rspec.version.add_slivers([str(hostname)])
449 elif status == node_status['remove']:
450 print "Remove hostname: %s" % hostname
451 rspec.version.remove_slivers([str(hostname)])
453 elif depth == 3: # Tag
454 tag, value = self.itemText(item).split(": ")
455 status = self.itemStatus(item)
456 tag = "%s" % tag # Prevent weird error from lxml
457 value = "%s" % value # Prevent weird error from lxml
458 hostname = self.itemText(item.parent())
459 testbed = self.itemText(item.parent().parent())
460 if status == tag_status['add']:
461 print "Add tag to (%s, %s): %s/%s " % (testbed, hostname, tag, value)
462 if hostname.startswith(default_tags):
463 rspec.version.add_default_sliver_attribute(tag, value, testbed)
465 node = rspec_node_names.get(hostname, None)
467 rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed)
469 elif status == tag_status['remove']:
470 print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value)
471 if hostname.startswith(default_tags):
472 rspec.version.remove_default_sliver_attribute(tag, value, testbed)
474 node = rspec_node_names.get(hostname, None)
476 rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed)
479 children = item.rowCount()
480 for row in range(0, children):
481 status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
482 change = change or status
487 if self.checkRunningProcess():
490 rspec = SfiData().getSliceRSpec()
491 resources = SfiData().getResourcesRSpec()
493 resource_node_names = self.nodesByName(resources.version.get_nodes())
494 rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
496 change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
499 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
502 # Several aggregates have issues with the <statistics> section in the
503 # rspec, so make sure it's not there.
504 stats_elems = rspec.xml.xpath("//statistics")
505 if len(stats_elems)>0:
506 stats_elem = stats_elems[0]
507 parent = stats_elem.xpath("..")[0]
508 parent.remove(stats_elem)
510 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
511 self.process.applyRSpec(rspec)
512 self.setStatus("Sending slice data (RSpec). This will take some time...")
514 def submit_pg_compat(self):
515 if self.checkRunningProcess():
518 rspec = SfiData().getSliceRSpec()
519 resources = SfiData().getResourcesRSpec()
520 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
523 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
526 dlg = ClientSliceManager(self)
527 dlg.submit_pg_compat(rspec)
530 self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
531 (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
532 QTimer.singleShot(2500, self.refresh)
535 dlg = RenewWindow(parent=self)
539 if not config.getSlice():
540 self.setStatus("<font color='red'>Slice not set yet!</font>")
543 if self.process.isRunning():
544 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
547 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
549 self.process.retrieveResources()
550 self.setStatus("Refreshing resources. This will take some time...")
552 def nodesByNetwork(self, nodeList):
554 for node in nodeList:
555 network_name = Xrn(node['component_manager_id']).get_hrn()
557 net = netDict.get(network_name, [])
559 netDict[network_name] = net
565 def nodesByName(self, nodeList, nameDict=None):
568 for node in nodeList:
569 hostname = node.get("component_name", None)
570 if hostname and (not hostname in nameDict):
571 nameDict[hostname] = node
575 def updateView(self):
576 global already_in_nodes
577 already_in_nodes = []
578 self.network_names = []
579 self.nodeModel.clear()
581 rspec = SfiData().getSliceRSpec()
585 resources = SfiData().getResourcesRSpec()
589 rootItem = self.nodeModel.invisibleRootItem()
592 for network in rspec.version.get_networks():
593 network_name = network.get("name", None)
594 if (network_name != None) and (not network_name in networks):
595 networks.append(network_name)
596 for network in resources.version.get_networks():
597 network_name = network.get("name", None)
598 if (network_name != None) and (not network_name in networks):
599 networks.append(network_name)
601 resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
602 rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
604 for network in networks:
605 self.network_names.append(network)
607 all_nodes = resources_nodes.get(network, [])
608 sliver_nodes = rspec_nodes.get(network, [])
610 sliver_node_names = self.nodesByName(sliver_nodes)
612 available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
614 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
615 networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
617 already_in_nodes += sliver_node_names.keys()
619 # Add default slice tags
620 nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
621 attrs = rspec.version.get_default_sliver_attributes(network)
623 name = attr.get("name", None)
624 value = attr.get("value", None)
625 tagstring = QString("%s: %s" % (name, value))
626 self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
628 for node in sliver_nodes:
630 if ("hardware_types" in node):
631 hardware_types = [x["name"] for x in node["hardware_types"]]
632 nodeType = ",".join(hardware_types)
633 nodeStatus = node.get("boot_state", "")
634 if nodeStatus == None:
636 nodeItem = self.nodeView.appendRow(networkItem,
637 node["component_name"],
638 nodeStatus=nodeStatus,
640 membership=node_status['in'],
643 attrs = rspec.version.get_sliver_attributes(node['component_id'], network)
645 name = attr.get("name", None)
646 value = attr.get("value", None)
647 self.nodeView.appendRow(nodeItem,
648 "%s: %s" % (name, value),
649 membership=tag_status['in'],
652 for node in available_nodes:
654 if ("hardware_types" in node):
655 hardware_types = [x["name"] for x in node["hardware_types"]]
656 nodeType = ",".join(hardware_types)
657 nodeStatus = node.get("boot_state", "")
658 if nodeStatus == None:
660 self.nodeView.appendRow(networkItem,
661 node["component_name"],
662 nodeStatus=nodeStatus,
664 membership=node_status['out'],
667 self.filterModel.setSourceModel(self.nodeModel)
668 self.filterModel.setDynamicSortFilter(True)
669 self.filterModel.sort(NAME_COLUMN)
671 headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
672 self.nodeModel.setHorizontalHeaderLabels(headers)
674 self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
675 self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
676 self.nodeView.setModel(self.filterModel)
677 self.nodeView.hideColumn(KIND_COLUMN)
678 self.nodeView.expandAll()
679 self.nodeView.resizeColumnToContents(0)
680 self.nodeView.collapseAll()
682 def updateSliceName(self):
683 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
685 def nodeSelectionChanged(self, hostname):
686 self.parent().nodeSelectionChanged(hostname)
688 class MainScreen(SfaScreen):
689 def __init__(self, parent):
690 SfaScreen.__init__(self, parent)
692 slice = SliceWidget(self)
693 self.init(slice, "Nodes", "OneLab SFA crawler")
695 def rspecUpdated(self):
696 self.mainwin.rspecWindow.updateView()
698 def configurationChanged(self):
699 self.widget.updateSliceName()
700 self.widget.updateView()
701 self.mainwin.rspecWindow.updateView()
703 def nodeSelectionChanged(self, hostname):
704 self.mainwin.nodeSelectionChanged(hostname)