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()
80 # probably no rspec downloaded yet
83 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
84 status_data = status_index.data().toString()
85 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
86 node_data = node_index.data().toString()
88 if itemType(node_index) == "tag":
89 data = node_index.data().toString()
90 tagname, value = data.split(": ")
91 if tagname not in settable_tags:
93 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
95 if status_data == tag_status['in']:
96 model.setData(status_index, QString(tag_status['remove']))
97 elif status_data == tag_status['add']:
98 model.setData(status_index, QString(tag_status['out']))
99 elif status_data == tag_status['remove']:
100 model.setData(status_index, QString(tag_status['in']))
101 else: model.setData(status_index, QString(node_status['out']))
104 if status_data == node_status['in']:
105 model.setData(status_index, QString(node_status['remove']))
106 elif status_data == node_status['out']:
107 model.setData(status_index, QString(node_status['add']))
108 elif status_data in (node_status['add'], node_status['remove']):
109 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
110 else: model.setData(status_index, QString(node_status['out']))
112 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
114 def appendRow(self, parent, name, nodeStatus="", nodeType="", membership="", kind = ""):
115 # row: name nodeStatus nodeType membership kind
116 item = QStandardItem(QString(str(name)))
118 QStandardItem(QString(str(nodeType))),
119 QStandardItem(QString(str(nodeStatus))),
120 QStandardItem(QString(str(membership))),
121 QStandardItem(QString(str(kind)))]
122 parent.appendRow(row)
125 def mousePressEvent(self, event):
126 QTreeView.mousePressEvent(self, event)
127 if event.button() == Qt.LeftButton:
131 index = self.currentIndex()
132 model = index.model()
135 # probably no rspec downloaded yet
138 status_index = model.index(index.row(), 1, index.parent())
139 status_data = status_index.data().toString()
140 node_index = model.index(index.row(), 0, index.parent())
141 node_data = node_index.data().toString()
143 if itemType(node_index) == "node":
145 if status_data in (node_status['in'], node_status['add'], ""):
146 # Pop up a dialog box for adding a new attribute
147 tagname, ok = QInputDialog.getItem(self, "Add tag",
148 "Tag name:", settable_tags)
150 value, ok = QInputDialog.getText(self, "Add tag",
151 "Value for tag '%s'" % tagname)
153 # We're using the QSortFilterProxyModel here
154 src_index = model.mapToSource(index)
155 src_model = src_index.model()
156 nodeItem = src_model.itemFromIndex(src_index)
158 self.appendRow(nodeItem, "%s: %s" % (tagname, value), membership=tag_status['add'], kind="attribute")
160 elif status_data in (node_status['out'], node_status['remove']):
161 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
164 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
166 def currentChanged(self, current, previous):
167 model = current.model()
168 node_index = model.index(current.row(), 0, current.parent())
169 node_data = node_index.data().toString()
170 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
174 class NodeNameDelegate(QStyledItemDelegate):
175 def __init__(self, parent):
176 QStyledItemDelegate.__init__(self, parent)
178 def displayText(self, value, locale):
179 data = str(QStyledItemDelegate.displayText(self, value, locale))
180 if (len(data)>NAME_MAX_LEN):
181 data = data[:(NAME_MAX_LEN-3)] + "..."
184 def paint(self, painter, option, index):
185 model = index.model()
186 data = str(self.displayText(index.data(), QLocale()))
187 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
188 status_data = status_index.data().toString()
190 fm = QFontMetrics(option.font)
191 rect = QRect(option.rect)
193 rect.setHeight(rect.height() - 2)
194 rect.setWidth(fm.width(QString(data)) + 6)
195 rect.setX(rect.x() + 5)
196 rect.setY(rect.y() - 1)
198 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
200 path = QPainterPath()
201 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
204 painter.setRenderHint(QPainter.Antialiasing)
206 if option.state & QStyle.State_Selected:
207 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
209 if itemType(index) == "node":
210 for x in node_status.keys():
211 if (node_status[x] == status_data) and (x in color_status):
212 painter.fillPath(path, color_status[x])
214 painter.setPen(QColor.fromRgb(0, 0, 0))
215 painter.drawText(rect, 0, QString(data))
218 for x in tag_status.keys():
219 if (tag_status[x] == status_data) and (x in color_status):
220 painter.fillPath(path, color_status[x])
222 painter.setPen(QColor.fromRgb(0, 0, 0))
223 painter.drawText(rect, 0, QString(data))
227 class NodeStatusDelegate(QStyledItemDelegate):
228 def __init__(self, parent):
229 QStyledItemDelegate.__init__(self, parent)
231 def paint(self, painter, option, index):
232 model = index.model()
233 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
234 nodestatus_data = nodestatus_index.data().toString()
236 fm = QFontMetrics(option.font)
237 rect = QRect(option.rect)
239 data = index.data().toString()
240 rect.setHeight(rect.height() - 2)
241 rect.setWidth(fm.width(QString(data)) + 6)
242 rect.setX(rect.x() + 5)
243 rect.setY(rect.y() - 1)
245 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
247 path = QPainterPath()
248 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
251 painter.setRenderHint(QPainter.Antialiasing)
253 if option.state & QStyle.State_Selected:
254 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
256 if (nodestatus_data == ""):
257 painter.setPen(QColor.fromRgb(0, 0, 0))
258 painter.drawText(rect, 0, QString(data))
259 elif (nodestatus_data == "boot"):
260 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
261 painter.setPen(QColor.fromRgb(0, 0, 0))
262 painter.drawText(rect, 0, QString(data))
264 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
265 painter.setPen(QColor.fromRgb(0, 0, 0))
266 painter.drawText(rect, 0, QString(data))
270 class NodeFilterProxyModel(QSortFilterProxyModel):
271 def __init__(self, parent=None):
272 QSortFilterProxyModel.__init__(self, parent)
273 self.hostname_filter_regex = None
274 self.nodestatus_filter = None
276 def setHostNameFilter(self, hostname):
277 self.hostname_filter_regex = QRegExp(hostname)
278 self.invalidateFilter()
280 def setNodeStatusFilter(self, status):
281 if (status == "all"):
282 self.nodestatus_filter = None
284 self.nodestatus_filter = status
285 self.invalidateFilter()
287 def filterAcceptsRow(self, sourceRow, source_parent):
288 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
289 if (kind_data == "node"):
290 if self.hostname_filter_regex:
291 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
292 if (self.hostname_filter_regex.indexIn(name_data) < 0):
294 if self.nodestatus_filter:
295 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
296 if (nodestatus_data != self.nodestatus_filter):
300 def lessThan(self, left, right):
301 l_str = str(left.data().toString())
302 r_str = str(right.data().toString())
304 # make sure default_tags appears before everything else
305 if l_str.startswith(default_tags):
308 if r_str.startswith(default_tags):
311 return (l_str < r_str)
314 class SliceWidget(QWidget):
315 def __init__(self, parent):
316 QWidget.__init__(self, parent)
318 self.network_names = []
319 self.process = SfiProcess(self)
321 self.slicename = QLabel("", self)
322 self.updateSliceName()
323 self.slicename.setScaledContents(False)
324 filterlabel = QLabel ("Filter: ", self)
325 filterbox = QComboBox(self)
326 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
327 searchlabel = QLabel ("Search: ", self)
328 searchlabel.setScaledContents(False)
329 searchbox = QLineEdit(self)
330 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
332 toplayout = QHBoxLayout()
333 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
334 toplayout.addStretch()
335 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
336 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
337 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
338 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
340 self.nodeView = NodeView(self)
341 self.nodeModel = QStandardItemModel(0, 4, self)
342 self.filterModel = NodeFilterProxyModel(self)
344 self.nodeNameDelegate = NodeNameDelegate(self)
345 self.nodeStatusDelegate = NodeStatusDelegate(self)
347 refresh = QPushButton("Refresh Slice Data", self)
348 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
349 renew = QPushButton("Renew Slice", self)
350 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
351 submit = QPushButton("Submit", self)
352 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
354 bottomlayout = QHBoxLayout()
355 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
356 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
357 bottomlayout.addStretch()
358 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
360 layout = QVBoxLayout()
361 layout.addLayout(toplayout)
362 layout.addWidget(self.nodeView)
363 layout.addLayout(bottomlayout)
364 self.setLayout(layout)
365 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
367 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
368 self.connect(renew, SIGNAL('clicked()'), self.renew)
369 self.connect(submit, SIGNAL('clicked()'), self.submit) # _pg_compat)
370 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
371 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
372 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
373 self.nodeSelectionChanged)
377 def submitFinished(self):
378 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
380 faultString = self.process.getFaultString()
382 self.setStatus("<font color='green'>Slice data submitted.</font>")
384 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
387 self.parent().signalAll("rspecUpdated")
389 def refreshResourcesFinished(self):
390 self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
392 faultString = self.process.getFaultString()
394 self.setStatus("Refreshing slice RSpec.")
395 self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
396 self.process.retrieveRspec()
398 self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
400 def refreshRSpecFinished(self):
401 self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
403 faultString = self.process.getFaultString()
405 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
407 self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
410 self.parent().signalAll("rspecUpdated")
412 def setStatus(self, msg, timeout=None):
413 self.parent().setStatus(msg, timeout)
415 def checkRunningProcess(self):
416 if self.process.isRunning():
417 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
421 def search(self, search_string):
422 self.filterModel.setHostNameFilter(str(search_string))
424 def filter(self, filter_string):
425 self.filterModel.setNodeStatusFilter(str(filter_string))
427 def itemStatus(self, item):
428 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
429 return str(statusItem.data(Qt.DisplayRole).toString())
431 def itemText(self, item):
432 return str(item.data(Qt.DisplayRole).toString())
434 # Recursively walk the tree, making changes to the RSpec
435 def process_subtree(self, rspec, rspec_node_names, resource_node_names, item, depth = 0):
437 model = self.nodeModel
441 elif depth == 2: # Hostname
442 hostname = self.itemText(item)
443 testbed = self.itemText(item.parent())
444 status = self.itemStatus(item)
445 if status == node_status['add']:
446 print "Add hostname: %s" % hostname
448 resource_node = resource_node_names.get(hostname, None)
450 if resource_node==None:
451 print "Error: Failed to find %s in resources rspec" % hostname
453 if not (hostname in rspec_node_names):
454 network_name = Xrn(resource_node['component_manager_id']).get_hrn()
455 rspec.version.add_network(network_name)
456 rspec.version.add_nodes([resource_node])
457 rspec.version.add_slivers([str(hostname)])
459 elif status == node_status['remove']:
460 print "Remove hostname: %s" % hostname
461 rspec.version.remove_slivers([str(hostname)])
463 elif depth == 3: # Tag
464 tag, value = self.itemText(item).split(": ")
465 status = self.itemStatus(item)
466 tag = "%s" % tag # Prevent weird error from lxml
467 value = "%s" % value # Prevent weird error from lxml
468 hostname = self.itemText(item.parent())
469 testbed = self.itemText(item.parent().parent())
470 if status == tag_status['add']:
471 print "Add tag to (%s, %s): %s/%s " % (testbed, hostname, tag, value)
472 if hostname.startswith(default_tags):
473 rspec.version.add_default_sliver_attribute(tag, value, testbed)
475 node = rspec_node_names.get(hostname, None)
477 rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed)
479 elif status == tag_status['remove']:
480 print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value)
481 if hostname.startswith(default_tags):
482 rspec.version.remove_default_sliver_attribute(tag, value, testbed)
484 node = rspec_node_names.get(hostname, None)
486 rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed)
489 children = item.rowCount()
490 for row in range(0, children):
491 status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
492 change = change or status
497 if self.checkRunningProcess():
500 rspec = SfiData().getSliceRSpec()
501 resources = SfiData().getResourcesRSpec()
503 resource_node_names = self.nodesByName(resources.version.get_nodes())
504 rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
506 change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
509 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
512 # Several aggregates have issues with the <statistics> section in the
513 # rspec, so make sure it's not there.
514 stats_elems = rspec.xml.xpath("//statistics")
515 if len(stats_elems)>0:
516 stats_elem = stats_elems[0]
517 parent = stats_elem.xpath("..")[0]
518 parent.remove(stats_elem)
520 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
521 self.process.applyRSpec(rspec)
522 self.setStatus("Sending slice data (RSpec). This will take some time...")
524 def submit_pg_compat(self):
525 if self.checkRunningProcess():
528 rspec = SfiData().getSliceRSpec()
529 resources = SfiData().getResourcesRSpec()
530 change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
533 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
536 dlg = ClientSliceManager(self)
537 dlg.submit_pg_compat(rspec)
540 self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
541 (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
542 QTimer.singleShot(2500, self.refresh)
545 dlg = RenewWindow(parent=self)
549 if not config.getSlice():
550 self.setStatus("<font color='red'>Slice not set yet!</font>")
553 if self.process.isRunning():
554 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
557 self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
559 self.process.retrieveResources()
560 self.setStatus("Refreshing resources. This will take some time...")
562 def nodesByNetwork(self, nodeList):
564 for node in nodeList:
565 network_name = Xrn(node['component_manager_id']).get_hrn()
567 net = netDict.get(network_name, [])
569 netDict[network_name] = net
575 def nodesByName(self, nodeList, nameDict=None):
578 for node in nodeList:
579 hostname = node.get("component_name", None)
580 if hostname and (not hostname in nameDict):
581 nameDict[hostname] = node
585 def updateView(self):
586 global already_in_nodes
587 already_in_nodes = []
588 self.network_names = []
589 self.nodeModel.clear()
591 rspec = SfiData().getSliceRSpec()
595 resources = SfiData().getResourcesRSpec()
599 rootItem = self.nodeModel.invisibleRootItem()
602 for network in rspec.version.get_networks():
603 network_name = network.get("name", None)
604 if (network_name != None) and (not network_name in networks):
605 networks.append(network_name)
606 for network in resources.version.get_networks():
607 network_name = network.get("name", None)
608 if (network_name != None) and (not network_name in networks):
609 networks.append(network_name)
611 resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
612 rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
614 for network in networks:
615 self.network_names.append(network)
617 all_nodes = resources_nodes.get(network, [])
618 sliver_nodes = rspec_nodes.get(network, [])
620 sliver_node_names = self.nodesByName(sliver_nodes)
622 available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
624 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
625 networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
627 already_in_nodes += sliver_node_names.keys()
629 # Add default slice tags
630 nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
631 attrs = rspec.version.get_default_sliver_attributes(network)
633 name = attr.get("name", None)
634 value = attr.get("value", None)
635 tagstring = QString("%s: %s" % (name, value))
636 self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
638 for node in sliver_nodes:
640 if ("hardware_types" in node):
641 hardware_types = [x["name"] for x in node["hardware_types"]]
642 nodeType = ",".join(hardware_types)
643 nodeStatus = node.get("boot_state", "")
644 if nodeStatus == None:
646 nodeItem = self.nodeView.appendRow(networkItem,
647 node["component_name"],
648 nodeStatus=nodeStatus,
650 membership=node_status['in'],
653 attrs = rspec.version.get_sliver_attributes(node['component_id'], network)
655 name = attr.get("name", None)
656 value = attr.get("value", None)
657 self.nodeView.appendRow(nodeItem,
658 "%s: %s" % (name, value),
659 membership=tag_status['in'],
662 for node in available_nodes:
664 if ("hardware_types" in node):
665 hardware_types = [x["name"] for x in node["hardware_types"]]
666 nodeType = ",".join(hardware_types)
667 nodeStatus = node.get("boot_state", "")
668 if nodeStatus == None:
670 self.nodeView.appendRow(networkItem,
671 node["component_name"],
672 nodeStatus=nodeStatus,
674 membership=node_status['out'],
677 self.filterModel.setSourceModel(self.nodeModel)
678 self.filterModel.setDynamicSortFilter(True)
679 self.filterModel.sort(NAME_COLUMN)
681 headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
682 self.nodeModel.setHorizontalHeaderLabels(headers)
684 self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
685 self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
686 self.nodeView.setModel(self.filterModel)
687 self.nodeView.hideColumn(KIND_COLUMN)
688 self.nodeView.expandAll()
689 self.nodeView.resizeColumnToContents(0)
690 self.nodeView.collapseAll()
692 def updateSliceName(self):
693 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
695 def nodeSelectionChanged(self, hostname):
696 self.parent().nodeSelectionChanged(hostname)
698 class MainScreen(SfaScreen):
699 def __init__(self, parent):
700 SfaScreen.__init__(self, parent)
702 slice = SliceWidget(self)
703 self.init(slice, "Nodes", "OneLab SFA crawler")
705 def rspecUpdated(self):
706 self.mainwin.rspecWindow.updateView()
708 def configurationChanged(self):
709 self.widget.updateSliceName()
710 self.widget.updateView()
711 self.mainwin.rspecWindow.updateView()
713 def nodeSelectionChanged(self, hostname):
714 self.mainwin.nodeSelectionChanged(hostname)