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()
if (model == None):
# probably no rspec downloaded yet
return
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()
if (model == None):
# probably no rspec downloaded yet
return
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 deleteSliversFinished(self):
self.disconnect(self.process, SIGNAL('finished()'), self.deleteSliversFinished)
faultString = self.process.getFaultString()
if not faultString:
self.setStatus("Slice data submitted.")
QTimer.singleShot(2500, self.refresh)
else:
self.setStatus("Slice submit failed: %s" % (faultString))
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):
changeAdd = 0
changeRemove = 0
changeAddTag = 0
changeRemoveTag = 0
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)])
changeAdd += 1
elif status == node_status['remove']:
print "Remove hostname: %s" % hostname
rspec.version.remove_slivers([str(hostname)])
changeRemove += 1
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)
changeAddTag += 1
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)
changeRemoveTag += 1
children = item.rowCount()
for row in range(0, children):
(c, ca, cr, cat, crt) = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
changeAdd = changeAdd + ca
changeRemove = changeRemove + cr
changeAddTag = changeAddTag + cat
changeRemoveTag = changeRemoveTag + crt
change = changeAdd + changeRemove + changeAddTag + changeRemoveTag
return (change, changeAdd, changeRemove, changeAddTag, changeRemoveTag)
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, changeAdd, changeRemove, changeAddTag, changeRemoveTag) = \
self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
if (change <= 0):
self.setStatus("No change in slice data. Not submitting!", timeout=3000)
return
if (changeRemove > 0) and (rspec.version.get_nodes_with_slivers() == []):
# removing the last sliver in a slice requires us to call deleteSlivers
self.connect(self.process, SIGNAL('finished()'), self.deleteSliversFinished)
self.process.deleteSlivers()
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, changeAdd, changeRemove, changeAddTag, changeRemoveTag) = \
self.process_subtree(rspec, rspec, resources, self.nodeModel.invisibleRootItem())
if (change <= 0):
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:
nodeType = None
if ("hardware_types" in node):
hardware_types = [x["name"] for x in node["hardware_types"]]
nodeType = ",".join(hardware_types)
nodeStatus = node.get("boot_state", "")
if nodeStatus == None:
nodeStatus=""
nodeItem = self.nodeView.appendRow(networkItem,
node["component_name"],
nodeStatus=nodeStatus,
nodeType=nodeType,
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:
nodeType = None
if ("hardware_types" in node):
hardware_types = [x["name"] for x in node["hardware_types"]]
nodeType = ",".join(hardware_types)
nodeStatus = node.get("boot_state", "")
if nodeStatus == None:
nodeStatus=""
self.nodeView.appendRow(networkItem,
node["component_name"],
nodeStatus=nodeStatus,
nodeType=nodeType,
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)