import datetime import os import urlparse from PyQt4.QtCore import * from PyQt4.QtGui import * #from sfa.util.rspecHelper import RSpec from sfa.util.xrn import Xrn from sface.config import config from sface.sfirenew import RenewWindow from sface.sfiprocess import SfiProcess from sface.screens.sfascreen import SfaScreen from sface.sfidata import SfiData from sface.clislicemgr import ClientSliceManager already_in_nodes = [] node_status = { "in": "Already Selected", "out": "Not Selected", "add": "To be Added", "remove": "To be Removed"} tag_status = { "in": "Already Set", "out": "Not Set", "add": "To be Added", "remove": "To be Removed"} color_status = { "in": QColor.fromRgb(0, 250, 250), "add": QColor.fromRgb(0, 250, 0), "remove": QColor.fromRgb(250, 0, 0) } default_tags = "Default tags" settable_tags = ['delegations', 'initscript'] NAME_COLUMN = 0 NODE_TYPE_COLUMN = 1 NODE_STATUS_COLUMN = 2 MEMBERSHIP_STATUS_COLUMN = 3 KIND_COLUMN = 4 # maximum length of a name to display before clipping NAME_MAX_LEN = 48 def itemType(index): if index.parent().parent().isValid(): return "tag" else: return "node" class NodeView(QTreeView): def __init__(self, parent): QTreeView.__init__(self, parent) self.setAnimated(True) self.setItemsExpandable(True) self.setRootIsDecorated(True) self.setAlternatingRowColors(True) # self.setSelectionMode(self.MultiSelection) self.setAttribute(Qt.WA_MacShowFocusRect, 0) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.") def keyPressEvent(self, event): if (event.key() == Qt.Key_Space): self.toggleSelection() else: QTreeView.keyPressEvent(self, event) def mouseDoubleClickEvent(self, event): self.toggleSelection() def toggleSelection(self): index = self.currentIndex() model = index.model() status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent()) status_data = status_index.data().toString() node_index = model.index(index.row(), NAME_COLUMN, index.parent()) node_data = node_index.data().toString() if itemType(node_index) == "tag": data = node_index.data().toString() tagname, value = data.split(": ") if tagname not in settable_tags: # Pop up error msg QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname) return if status_data == tag_status['in']: model.setData(status_index, QString(tag_status['remove'])) elif status_data == tag_status['add']: model.setData(status_index, QString(tag_status['out'])) elif status_data == tag_status['remove']: model.setData(status_index, QString(tag_status['in'])) else: model.setData(status_index, QString(node_status['out'])) else: # This is a hostname if status_data == node_status['in']: model.setData(status_index, QString(node_status['remove'])) elif status_data == node_status['out']: model.setData(status_index, QString(node_status['add'])) elif status_data in (node_status['add'], node_status['remove']): if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in'])) else: model.setData(status_index, QString(node_status['out'])) model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index) def appendRow(self, parent, name, nodeStatus="", nodeType="", membership="", kind = ""): # row: name nodeStatus nodeType membership kind item = QStandardItem(QString(str(name))) row = [item, QStandardItem(QString(str(nodeType))), QStandardItem(QString(str(nodeStatus))), QStandardItem(QString(str(membership))), QStandardItem(QString(str(kind)))] parent.appendRow(row) return item def mousePressEvent(self, event): QTreeView.mousePressEvent(self, event) if event.button() == Qt.LeftButton: return # Right click index = self.currentIndex() model = index.model() status_index = model.index(index.row(), 1, index.parent()) status_data = status_index.data().toString() node_index = model.index(index.row(), 0, index.parent()) node_data = node_index.data().toString() if itemType(node_index) == "node": # This is a hostname if status_data in (node_status['in'], node_status['add'], ""): # Pop up a dialog box for adding a new attribute tagname, ok = QInputDialog.getItem(self, "Add tag", "Tag name:", settable_tags) if ok: value, ok = QInputDialog.getText(self, "Add tag", "Value for tag '%s'" % tagname) if ok: # We're using the QSortFilterProxyModel here src_index = model.mapToSource(index) src_model = src_index.model() nodeItem = src_model.itemFromIndex(src_index) self.appendRow(nodeItem, "%s: %s" % (tagname, value), membership=tag_status['add'], kind="attribute") elif status_data in (node_status['out'], node_status['remove']): QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes") return model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index) def currentChanged(self, current, previous): model = current.model() node_index = model.index(current.row(), 0, current.parent()) node_data = node_index.data().toString() self.emit(SIGNAL('hostnameClicked(QString)'), node_data) class NodeNameDelegate(QStyledItemDelegate): def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) def displayText(self, value, locale): data = str(QStyledItemDelegate.displayText(self, value, locale)) if (len(data)>NAME_MAX_LEN): data = data[:(NAME_MAX_LEN-3)] + "..." return QString(data) def paint(self, painter, option, index): model = index.model() data = str(self.displayText(index.data(), QLocale())) status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent()) status_data = status_index.data().toString() fm = QFontMetrics(option.font) rect = QRect(option.rect) rect.setHeight(rect.height() - 2) rect.setWidth(fm.width(QString(data)) + 6) rect.setX(rect.x() + 5) rect.setY(rect.y() - 1) x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width() path = QPainterPath() path.addRoundedRect(x - 1, y + 1, w, h, 4, 4) painter.save() painter.setRenderHint(QPainter.Antialiasing) if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight)) if itemType(index) == "node": for x in node_status.keys(): if (node_status[x] == status_data) and (x in color_status): painter.fillPath(path, color_status[x]) painter.setPen(QColor.fromRgb(0, 0, 0)) painter.drawText(rect, 0, QString(data)) else: for x in tag_status.keys(): if (tag_status[x] == status_data) and (x in color_status): painter.fillPath(path, color_status[x]) painter.setPen(QColor.fromRgb(0, 0, 0)) painter.drawText(rect, 0, QString(data)) painter.restore() class NodeStatusDelegate(QStyledItemDelegate): def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) def paint(self, painter, option, index): model = index.model() nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent()) nodestatus_data = nodestatus_index.data().toString() fm = QFontMetrics(option.font) rect = QRect(option.rect) data = index.data().toString() rect.setHeight(rect.height() - 2) rect.setWidth(fm.width(QString(data)) + 6) rect.setX(rect.x() + 5) rect.setY(rect.y() - 1) x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width() path = QPainterPath() path.addRoundedRect(x - 1, y + 1, w, h, 4, 4) painter.save() painter.setRenderHint(QPainter.Antialiasing) if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight)) if (nodestatus_data == ""): painter.setPen(QColor.fromRgb(0, 0, 0)) painter.drawText(rect, 0, QString(data)) elif (nodestatus_data == "boot"): painter.fillPath(path, QColor.fromRgb(0, 250, 0)) painter.setPen(QColor.fromRgb(0, 0, 0)) painter.drawText(rect, 0, QString(data)) else: painter.fillPath(path, QColor.fromRgb(250, 0, 0)) painter.setPen(QColor.fromRgb(0, 0, 0)) painter.drawText(rect, 0, QString(data)) painter.restore() class NodeFilterProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): QSortFilterProxyModel.__init__(self, parent) self.hostname_filter_regex = None self.nodestatus_filter = None def setHostNameFilter(self, hostname): self.hostname_filter_regex = QRegExp(hostname) self.invalidateFilter() def setNodeStatusFilter(self, status): if (status == "all"): self.nodestatus_filter = None else: self.nodestatus_filter = status self.invalidateFilter() def filterAcceptsRow(self, sourceRow, source_parent): kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString() if (kind_data == "node"): if self.hostname_filter_regex: name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString() if (self.hostname_filter_regex.indexIn(name_data) < 0): return False if self.nodestatus_filter: nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString() if (nodestatus_data != self.nodestatus_filter): return False return True def lessThan(self, left, right): l_str = str(left.data().toString()) r_str = str(right.data().toString()) # make sure default_tags appears before everything else if l_str.startswith(default_tags): return True if r_str.startswith(default_tags): return False return (l_str < r_str) class SliceWidget(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self.network_names = [] self.process = SfiProcess(self) self.slicename = QLabel("", self) self.updateSliceName() self.slicename.setScaledContents(False) filterlabel = QLabel ("Filter: ", self) filterbox = QComboBox(self) filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"]) searchlabel = QLabel ("Search: ", self) searchlabel.setScaledContents(False) searchbox = QLineEdit(self) searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0) toplayout = QHBoxLayout() toplayout.addWidget(self.slicename, 0, Qt.AlignLeft) toplayout.addStretch() toplayout.addWidget(filterlabel, 0, Qt.AlignRight) toplayout.addWidget(filterbox, 0, Qt.AlignRight) toplayout.addWidget(searchlabel, 0, Qt.AlignRight) toplayout.addWidget(searchbox, 0, Qt.AlignRight) self.nodeView = NodeView(self) self.nodeModel = QStandardItemModel(0, 4, self) self.filterModel = NodeFilterProxyModel(self) self.nodeNameDelegate = NodeNameDelegate(self) self.nodeStatusDelegate = NodeStatusDelegate(self) refresh = QPushButton("Refresh Slice Data", self) refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) renew = QPushButton("Renew Slice", self) renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) submit = QPushButton("Submit", self) submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) bottomlayout = QHBoxLayout() bottomlayout.addWidget(refresh, 0, Qt.AlignLeft) bottomlayout.addWidget(renew, 0, Qt.AlignLeft) bottomlayout.addStretch() bottomlayout.addWidget(submit, 0, Qt.AlignRight) layout = QVBoxLayout() layout.addLayout(toplayout) layout.addWidget(self.nodeView) layout.addLayout(bottomlayout) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.connect(refresh, SIGNAL('clicked()'), self.refresh) self.connect(renew, SIGNAL('clicked()'), self.renew) self.connect(submit, SIGNAL('clicked()'), self.submit) # _pg_compat) self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search) self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter) self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'), self.nodeSelectionChanged) self.updateView() def submitFinished(self): self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished) faultString = self.process.getFaultString() if not faultString: self.setStatus("Slice data submitted.") else: self.setStatus("Slice submit failed: %s" % (faultString)) self.updateView() self.parent().signalAll("rspecUpdated") def refreshResourcesFinished(self): self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished) faultString = self.process.getFaultString() if not faultString: self.setStatus("Refreshing slice RSpec.") self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished) self.process.retrieveRspec() else: self.setStatus("Resources refresh failed: %s" % (faultString)) def refreshRSpecFinished(self): self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished) faultString = self.process.getFaultString() if not faultString: self.setStatus("Slice data refreshed.", timeout=5000) else: self.setStatus("Slice refresh failed: %s" % (faultString)) self.updateView() self.parent().signalAll("rspecUpdated") def setStatus(self, msg, timeout=None): self.parent().setStatus(msg, timeout) def checkRunningProcess(self): if self.process.isRunning(): self.setStatus("There is already a process running. Please wait.") return True return False def search(self, search_string): self.filterModel.setHostNameFilter(str(search_string)) def filter(self, filter_string): self.filterModel.setNodeStatusFilter(str(filter_string)) def itemStatus(self, item): statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN) return str(statusItem.data(Qt.DisplayRole).toString()) def itemText(self, item): return str(item.data(Qt.DisplayRole).toString()) # Recursively walk the tree, making changes to the RSpec def process_subtree(self, rspec, rspec_node_names, resource_node_names, item, depth = 0): change = False model = self.nodeModel if depth in [0, 1]: pass elif depth == 2: # Hostname hostname = self.itemText(item) testbed = self.itemText(item.parent()) status = self.itemStatus(item) if status == node_status['add']: print "Add hostname: %s" % hostname resource_node = resource_node_names.get(hostname, None) if resource_node==None: print "Error: Failed to find %s in resources rspec" % hostname else: if not (hostname in rspec_node_names): network_name = Xrn(resource_node['component_manager_id']).get_hrn() rspec.version.add_network(network_name) rspec.version.add_nodes([resource_node]) rspec.version.add_slivers([str(hostname)]) change = True elif status == node_status['remove']: print "Remove hostname: %s" % hostname rspec.version.remove_slivers([str(hostname)]) change = True elif depth == 3: # Tag tag, value = self.itemText(item).split(": ") status = self.itemStatus(item) tag = "%s" % tag # Prevent weird error from lxml value = "%s" % value # Prevent weird error from lxml hostname = self.itemText(item.parent()) testbed = self.itemText(item.parent().parent()) if status == tag_status['add']: print "Add tag to (%s, %s): %s/%s " % (testbed, hostname, tag, value) if hostname.startswith(default_tags): rspec.version.add_default_sliver_attribute(tag, value, testbed) else: node = rspec_node_names.get(hostname, None) if node: rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed) change = True elif status == tag_status['remove']: print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value) if hostname.startswith(default_tags): rspec.version.remove_default_sliver_attribute(tag, value, testbed) else: node = rspec_node_names.get(hostname, None) if node: rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed) change = True children = item.rowCount() for row in range(0, children): status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1) change = change or status return change def submit(self): if self.checkRunningProcess(): return rspec = SfiData().getSliceRSpec() resources = SfiData().getResourcesRSpec() resource_node_names = self.nodesByName(resources.version.get_nodes()) rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers()) change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem()) if not change: self.setStatus("No change in slice data. Not submitting!", timeout=3000) return # Several aggregates have issues with the section in the # rspec, so make sure it's not there. stats_elems = rspec.xml.xpath("//statistics") if len(stats_elems)>0: stats_elem = stats_elems[0] parent = stats_elem.xpath("..")[0] parent.remove(stats_elem) self.connect(self.process, SIGNAL('finished()'), self.submitFinished) self.process.applyRSpec(rspec) self.setStatus("Sending slice data (RSpec). This will take some time...") def submit_pg_compat(self): if self.checkRunningProcess(): return rspec = SfiData().getSliceRSpec() resources = SfiData().getResourcesRSpec() change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem()) if not change: self.setStatus("No change in slice data. Not submitting!", timeout=3000) return dlg = ClientSliceManager(self) dlg.submit_pg_compat(rspec) dlg.exec_() self.setStatus("Finished submitting. %d/%d aggs succeeded." % (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount)) QTimer.singleShot(2500, self.refresh) def renew(self): dlg = RenewWindow(parent=self) dlg.exec_() def refresh(self): if not config.getSlice(): self.setStatus("Slice not set yet!") return if self.process.isRunning(): self.setStatus("There is already a process running. Please wait.") return self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished) self.process.retrieveResources() self.setStatus("Refreshing resources. This will take some time...") def nodesByNetwork(self, nodeList): netDict = {} for node in nodeList: network_name = Xrn(node['component_manager_id']).get_hrn() if network_name: net = netDict.get(network_name, []) if net == []: netDict[network_name] = net net.append(node) return netDict def nodesByName(self, nodeList, nameDict=None): if nameDict==None: nameDict = {} for node in nodeList: hostname = node.get("component_name", None) if hostname and (not hostname in nameDict): nameDict[hostname] = node return nameDict def updateView(self): global already_in_nodes already_in_nodes = [] self.network_names = [] self.nodeModel.clear() rspec = SfiData().getSliceRSpec() if not rspec: return None resources = SfiData().getResourcesRSpec() if not resources: return None rootItem = self.nodeModel.invisibleRootItem() networks = [] for network in rspec.version.get_networks(): network_name = network.get("name", None) if (network_name != None) and (not network_name in networks): networks.append(network_name) for network in resources.version.get_networks(): network_name = network.get("name", None) if (network_name != None) and (not network_name in networks): networks.append(network_name) resources_nodes = self.nodesByNetwork(resources.version.get_nodes()) rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers()) for network in networks: self.network_names.append(network) all_nodes = resources_nodes.get(network, []) sliver_nodes = rspec_nodes.get(network, []) sliver_node_names = self.nodesByName(sliver_nodes) available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ] msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes)) networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network") already_in_nodes += sliver_node_names.keys() # Add default slice tags nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults") attrs = rspec.version.get_default_sliver_attributes(network) for attr in attrs: name = attr.get("name", None) value = attr.get("value", None) tagstring = QString("%s: %s" % (name, value)) self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute") for node in sliver_nodes: nodeItem = self.nodeView.appendRow(networkItem, node["component_name"], nodeStatus=node.get("boot_state", ""), #nodeType=node.get("rspec.get_node_sliver_type(node, network), membership=node_status['in'], kind="node") attrs = rspec.version.get_sliver_attributes(node['component_id'], network) for attr in attrs: name = attr.get("name", None) value = attr.get("value", None) self.nodeView.appendRow(nodeItem, "%s: %s" % (name, value), membership=tag_status['in'], kind="attribute") for node in available_nodes: self.nodeView.appendRow(networkItem, node["component_name"], nodeStatus = node.get("boot_state", ""), #nodeType= resources.get_node_sliver_type(node, network), membership=node_status['out'], kind="node") self.filterModel.setSourceModel(self.nodeModel) self.filterModel.setDynamicSortFilter(True) self.filterModel.sort(NAME_COLUMN) headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind" self.nodeModel.setHorizontalHeaderLabels(headers) self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate) self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate) self.nodeView.setModel(self.filterModel) self.nodeView.hideColumn(KIND_COLUMN) self.nodeView.expandAll() self.nodeView.resizeColumnToContents(0) self.nodeView.collapseAll() def updateSliceName(self): self.slicename.setText("Slice : %s" % (config.getSlice() or "None")) def nodeSelectionChanged(self, hostname): self.parent().nodeSelectionChanged(hostname) class MainScreen(SfaScreen): def __init__(self, parent): SfaScreen.__init__(self, parent) slice = SliceWidget(self) self.init(slice, "Nodes", "OneLab SFA crawler") def rspecUpdated(self): self.mainwin.rspecWindow.updateView() def configurationChanged(self): self.widget.updateSliceName() self.widget.updateView() self.mainwin.rspecWindow.updateView() def nodeSelectionChanged(self, hostname): self.mainwin.nodeSelectionChanged(hostname)