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 hostname = 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, hostname, tag, value)
448 if hostname.startswith(default_tags):
449 rspec.version.add_default_sliver_attribute(tag, value, testbed)
451 node = rspec_node_names.get(hostname, None)
453 rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed)
455 elif status == tag_status['remove']:
456 print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value)
457 if hostname.startswith(default_tags):
458 rspec.version.remove_default_sliver_attribute(tag, value, testbed)
460 node = rspec_node_names.get(hostname, None)
462 rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed)
465 children = item.rowCount()
466 for row in range(0, children):
467 status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
468 change = change or status
473 if self.checkRunningProcess():
476 rspec = SfiData().getSliceRSpec()
477 resources = SfiData().getResourcesRSpec()
479 resource_node_names = self.nodesByName(resources.version.get_nodes())
480 rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
482 change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
485 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
488 # Several aggregates have issues with the <statistics> section in the
489 # rspec, so make sure it's not there.
490 stats_elems = rspec.xml.xpath("//statistics")
491 if len(stats_elems)>0:
492 stats_elem = stats_elems[0]
493 parent = stats_elem.xpath("..")[0]
494 parent.remove(stats_elem)
496 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
497 self.process.applyRSpec(rspec)
498 self.setStatus("Sending slice data (RSpec). This will take some time...")
500 def submit_pg_compat(self):
501 if self.checkRunningProcess():
504 rspec = SfiData().getSliceRSpec()
505 resources = SfiData().getResourcesRSpec()
506 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
509 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
512 dlg = ClientSliceManager(self)
513 dlg.submit_pg_compat(rspec)
516 self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
517 (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
518 QTimer.singleShot(2500, self.refresh)
521 dlg = RenewWindow(parent=self)
525 if not config.getSlice():
526 self.setStatus("<font color='red'>Slice not set yet!</font>")
529 if self.process.isRunning():
530 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
533 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
535 self.process.retrieveResources()
536 self.setStatus("Refreshing resources. This will take some time...")
538 def nodesByNetwork(self, nodeList):
540 for node in nodeList:
541 network_name = Xrn(node['component_manager_id']).get_hrn()
543 net = netDict.get(network_name, [])
545 netDict[network_name] = net
551 def nodesByName(self, nodeList, nameDict=None):
554 for node in nodeList:
555 hostname = node.get("component_name", None)
556 if hostname and (not hostname in nameDict):
557 nameDict[hostname] = node
561 def updateView(self):
562 global already_in_nodes
563 already_in_nodes = []
564 self.network_names = []
565 self.nodeModel.clear()
567 rspec = SfiData().getSliceRSpec()
571 resources = SfiData().getResourcesRSpec()
575 rootItem = self.nodeModel.invisibleRootItem()
578 for network in rspec.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)
582 for network in resources.get_networks():
583 network_name = network.get("name", None)
584 if (network_name != None) and (not network_name in networks):
585 networks.append(network_name)
587 resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
588 rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
590 for network in networks:
591 self.network_names.append(network)
593 all_nodes = resources_nodes.get(network, [])
594 sliver_nodes = rspec_nodes.get(network, [])
596 sliver_node_names = self.nodesByName(sliver_nodes)
598 available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
600 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
601 networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
603 already_in_nodes += sliver_node_names.keys()
605 # Add default slice tags
606 nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
607 attrs = rspec.get_default_sliver_attributes(network)
609 name = attr.get("name", None)
610 value = attr.get("value", None)
611 tagstring = QString("%s: %s" % (name, value))
612 self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
614 for node in sliver_nodes:
615 nodeItem = self.nodeView.appendRow(networkItem,
616 node["component_name"],
617 nodeStatus=node.get("boot_state", ""),
618 #nodeType=node.get("rspec.get_node_sliver_type(node, network),
619 membership=node_status['in'],
622 attrs = rspec.get_sliver_attributes(node['component_id'], network)
624 name = attr.get("name", None)
625 value = attr.get("value", None)
626 self.nodeView.appendRow(nodeItem,
627 "%s: %s" % (name, value),
628 membership=tag_status['in'],
631 for node in available_nodes:
632 self.nodeView.appendRow(networkItem,
633 node["component_name"],
634 nodeStatus = node.get("boot_state", ""),
635 #nodeType= resources.get_node_sliver_type(node, network),
636 membership=node_status['out'],
639 self.filterModel.setSourceModel(self.nodeModel)
640 self.filterModel.setDynamicSortFilter(True)
641 self.filterModel.sort(NAME_COLUMN)
643 headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
644 self.nodeModel.setHorizontalHeaderLabels(headers)
646 self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
647 self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
648 self.nodeView.setModel(self.filterModel)
649 self.nodeView.hideColumn(KIND_COLUMN)
650 self.nodeView.expandAll()
651 self.nodeView.resizeColumnToContents(0)
652 self.nodeView.collapseAll()
654 def updateSliceName(self):
655 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
657 def nodeSelectionChanged(self, hostname):
658 self.parent().nodeSelectionChanged(hostname)
660 class MainScreen(SfaScreen):
661 def __init__(self, parent):
662 SfaScreen.__init__(self, parent)
664 slice = SliceWidget(self)
665 self.init(slice, "Nodes", "OneLab SFA crawler")
667 def rspecUpdated(self):
668 self.mainwin.rspecWindow.updateView()
670 def configurationChanged(self):
671 self.widget.updateSliceName()
672 self.widget.updateView()
673 self.mainwin.rspecWindow.updateView()
675 def nodeSelectionChanged(self, hostname):
676 self.mainwin.nodeSelectionChanged(hostname)