4 from PyQt4.QtCore import *
5 from PyQt4.QtGui import *
7 #from sfa.util.rspecHelper import RSpec
8 from sfa.rspecs.rspec_parser import parse_rspec
9 from sface.config import config
10 from sface.sfirenew import RenewWindow
11 from sface.sfiprocess import SfiProcess
12 from sface.screens.sfascreen import SfaScreen
16 node_status = { "in": "Already Selected",
17 "out": "Not Selected",
19 "remove": "To be Removed"}
21 tag_status = { "in": "Already Set",
24 "remove": "To be Removed"}
26 default_tags = "Default tags"
27 settable_tags = ['delegations', 'initscript']
30 NODE_STATUS_COLUMN = 1
31 MEMBERSHIP_STATUS_COLUMN = 2
34 # maximum length of a name to display before clipping
38 if index.parent().parent().isValid():
44 class NodeView(QTreeView):
45 def __init__(self, parent):
46 QTreeView.__init__(self, parent)
48 self.setAnimated(True)
49 self.setItemsExpandable(True)
50 self.setRootIsDecorated(True)
51 self.setAlternatingRowColors(True)
52 # self.setSelectionMode(self.MultiSelection)
53 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
54 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
55 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
57 def mouseDoubleClickEvent(self, event):
58 index = self.currentIndex()
60 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
61 status_data = status_index.data().toString()
62 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
63 node_data = node_index.data().toString()
65 if itemType(node_index) == "tag":
66 data = node_index.data().toString()
67 tagname, value = data.split(": ")
68 if tagname not in settable_tags:
70 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
72 if status_data == tag_status['in']:
73 model.setData(status_index, QString(tag_status['remove']))
74 elif status_data == tag_status['add']:
75 model.setData(status_index, QString(tag_status['out']))
76 elif status_data == tag_status['remove']:
77 model.setData(status_index, QString(tag_status['in']))
78 else: model.setData(status_index, QString(node_status['out']))
81 if status_data == node_status['in']:
82 model.setData(status_index, QString(node_status['remove']))
83 elif status_data == node_status['out']:
84 model.setData(status_index, QString(node_status['add']))
85 elif status_data in (node_status['add'], node_status['remove']):
86 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
87 else: model.setData(status_index, QString(node_status['out']))
89 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
91 def mousePressEvent(self, event):
92 QTreeView.mousePressEvent(self, event)
93 if event.button() == Qt.LeftButton:
97 index = self.currentIndex()
99 status_index = model.index(index.row(), 1, index.parent())
100 status_data = status_index.data().toString()
101 node_index = model.index(index.row(), 0, index.parent())
102 node_data = node_index.data().toString()
104 if itemType(node_index) == "node":
106 if status_data in (node_status['in'], node_status['add'], ""):
107 # Pop up a dialog box for adding a new attribute
108 tagname, ok = QInputDialog.getItem(self, "Add tag",
109 "Tag name:", settable_tags)
111 value, ok = QInputDialog.getText(self, "Add tag",
112 "Value for tag '%s'" % tagname)
114 # Add a new row to the model for the tag
116 # For testing with the QStandardItemModel
117 #nodeItem = model.itemFromIndex(index)
118 #tagstring = QString("%s: %s" % (tagname, value))
119 #tagItem = QStandardItem(tagstring)
120 #status = QStandardItem(QString(tag_status['add']))
121 #nodeItem.appendRow([tagItem, status])
123 # We're using the QSortFilterProxyModel here
124 src_index = model.mapToSource(index)
125 src_model = src_index.model()
126 nodeItem = src_model.itemFromIndex(src_index)
127 tagstring = QString("%s: %s" % (tagname, value))
128 tagItem = QStandardItem(tagstring)
129 status = QStandardItem(QString(tag_status['add']))
130 nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
132 elif status_data in (node_status['out'], node_status['remove']):
133 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
136 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
138 def currentChanged(self, current, previous):
139 model = current.model()
140 node_index = model.index(current.row(), 0, current.parent())
141 node_data = node_index.data().toString()
142 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
146 class NodeNameDelegate(QStyledItemDelegate):
147 def __init__(self, parent):
148 QStyledItemDelegate.__init__(self, parent)
150 def displayText(self, value, locale):
151 data = str(QStyledItemDelegate.displayText(self, value, locale))
152 if (len(data)>NAME_MAX_LEN):
153 data = data[:(NAME_MAX_LEN-3)] + "..."
156 def paint(self, painter, option, index):
157 model = index.model()
158 data = str(self.displayText(index.data(), QLocale()))
159 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
160 status_data = status_index.data().toString()
162 fm = QFontMetrics(option.font)
163 rect = QRect(option.rect)
165 rect.setHeight(rect.height() - 2)
166 rect.setWidth(fm.width(QString(data)) + 6)
167 rect.setX(rect.x() + 5)
168 rect.setY(rect.y() - 1)
170 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
172 path = QPainterPath()
173 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
176 painter.setRenderHint(QPainter.Antialiasing)
178 if option.state & QStyle.State_Selected:
179 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
181 if itemType(index) == "node":
182 if status_data == node_status['in']: # already in the slice
183 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
184 painter.setPen(QColor.fromRgb(0, 0, 0))
185 painter.drawText(rect, 0, QString(data))
187 elif status_data == node_status['add']: # newly added to the slice
188 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
189 painter.setPen(QColor.fromRgb(0, 0, 0))
190 painter.drawText(rect, 0, QString(data))
192 elif status_data == node_status['remove']: # removed from the slice
193 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
194 painter.setPen(QColor.fromRgb(0, 0, 0))
195 painter.drawText(rect, 0, QString(data))
198 painter.setPen(QColor.fromRgb(0, 0, 0))
199 painter.drawText(rect, 0, QString(data))
202 if status_data == tag_status['in']: # already in the slice
203 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
204 painter.setPen(QColor.fromRgb(0, 0, 0))
205 painter.drawText(rect, 0, QString(data))
207 elif status_data == tag_status['add']: # newly added to the slice
208 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
209 painter.setPen(QColor.fromRgb(0, 0, 0))
210 painter.drawText(rect, 0, QString(data))
212 elif status_data == tag_status['remove']: # removed from the slice
213 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
214 painter.setPen(QColor.fromRgb(0, 0, 0))
215 painter.drawText(rect, 0, QString(data))
218 painter.setPen(QColor.fromRgb(0, 0, 0))
219 painter.drawText(rect, 0, QString(data))
223 class NodeStatusDelegate(QStyledItemDelegate):
224 def __init__(self, parent):
225 QStyledItemDelegate.__init__(self, parent)
227 def paint(self, painter, option, index):
228 model = index.model()
229 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
230 nodestatus_data = nodestatus_index.data().toString()
232 fm = QFontMetrics(option.font)
233 rect = QRect(option.rect)
235 data = index.data().toString()
236 rect.setHeight(rect.height() - 2)
237 rect.setWidth(fm.width(QString(data)) + 6)
238 rect.setX(rect.x() + 5)
239 rect.setY(rect.y() - 1)
241 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
243 path = QPainterPath()
244 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
247 painter.setRenderHint(QPainter.Antialiasing)
249 if option.state & QStyle.State_Selected:
250 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
252 if (nodestatus_data == ""):
253 painter.setPen(QColor.fromRgb(0, 0, 0))
254 painter.drawText(rect, 0, QString(data))
255 elif (nodestatus_data == "boot"):
256 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
257 painter.setPen(QColor.fromRgb(0, 0, 0))
258 painter.drawText(rect, 0, QString(data))
260 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
261 painter.setPen(QColor.fromRgb(0, 0, 0))
262 painter.drawText(rect, 0, QString(data))
266 class NodeFilterProxyModel(QSortFilterProxyModel):
267 def __init__(self, parent=None):
268 QSortFilterProxyModel.__init__(self, parent)
269 self.hostname_filter_regex = None
270 self.nodestatus_filter = None
272 def setHostNameFilter(self, hostname):
273 self.hostname_filter_regex = QRegExp(hostname)
274 self.invalidateFilter()
276 def setNodeStatusFilter(self, status):
277 if (status == "all"):
278 self.nodestatus_filter = None
280 self.nodestatus_filter = status
281 self.invalidateFilter()
283 def filterAcceptsRow(self, sourceRow, source_parent):
284 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
285 if (kind_data == "node"):
286 if self.hostname_filter_regex:
287 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
288 if (self.hostname_filter_regex.indexIn(name_data) < 0):
290 if self.nodestatus_filter:
291 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
292 if (nodestatus_data != self.nodestatus_filter):
296 class SliceWidget(QWidget):
297 def __init__(self, parent):
298 QWidget.__init__(self, parent)
300 self.network_names = []
301 self.process = SfiProcess(self)
303 self.slicename = QLabel("", self)
304 self.updateSliceName()
305 self.slicename.setScaledContents(False)
306 filterlabel = QLabel ("Filter: ", self)
307 filterbox = QComboBox(self)
308 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
309 searchlabel = QLabel ("Search: ", self)
310 searchlabel.setScaledContents(False)
311 searchbox = QLineEdit(self)
312 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
314 toplayout = QHBoxLayout()
315 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
316 toplayout.addStretch()
317 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
318 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
319 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
320 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
322 self.nodeView = NodeView(self)
323 self.nodeModel = QStandardItemModel(0, 4, self)
324 self.filterModel = NodeFilterProxyModel(self)
326 self.nodeNameDelegate = NodeNameDelegate(self)
327 self.nodeStatusDelegate = NodeStatusDelegate(self)
329 refresh = QPushButton("Refresh Slice Data", self)
330 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
331 renew = QPushButton("Renew Slice", self)
332 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
333 submit = QPushButton("Submit", self)
334 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
336 bottomlayout = QHBoxLayout()
337 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
338 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
339 bottomlayout.addStretch()
340 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
342 layout = QVBoxLayout()
343 layout.addLayout(toplayout)
344 layout.addWidget(self.nodeView)
345 layout.addLayout(bottomlayout)
346 self.setLayout(layout)
347 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
349 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
350 self.connect(renew, SIGNAL('clicked()'), self.renew)
351 self.connect(submit, SIGNAL('clicked()'), self.submit)
352 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
353 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
354 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
355 self.nodeSelectionChanged)
359 def submitFinished(self):
360 self.setStatus("<font color='green'>Slice data submitted.</font>")
361 QTimer.singleShot(1000, self.refresh)
363 def refreshFinished(self):
364 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
366 self.parent().signalAll("rspecUpdated")
368 def readSliceRSpec(self):
369 rspec_file = config.getSliceRSpecFile()
370 if os.path.exists(rspec_file):
371 xml = open(rspec_file).read()
372 return parse_rspec(xml)
375 def setStatus(self, msg, timeout=None):
376 self.parent().setStatus(msg, timeout)
378 def checkRunningProcess(self):
379 if self.process.isRunning():
380 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
384 def search(self, search_string):
385 self.filterModel.setHostNameFilter(str(search_string))
387 def filter(self, filter_string):
388 self.filterModel.setNodeStatusFilter(str(filter_string))
390 def itemStatus(self, item):
391 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
392 return statusItem.data(Qt.DisplayRole).toString()
394 def itemText(self, item):
395 return item.data(Qt.DisplayRole).toString()
397 # Recursively walk the tree, making changes to the RSpec
398 def process_subtree(self, rspec, item, depth = 0):
400 model = self.nodeModel
404 elif depth == 2: # Hostname
405 hostname = self.itemText(item)
406 testbed = self.itemText(item.parent())
407 status = self.itemStatus(item)
408 if status == node_status['add']:
409 print "Add hostname: %s" % hostname
410 rspec.add_slivers(str(hostname), testbed)
412 elif status == node_status['remove']:
413 print "Remove hostname: %s" % hostname
414 rspec.remove_slivers(str(hostname), testbed)
416 elif depth == 3: # Tag
417 tag, value = self.itemText(item).split(": ")
418 status = self.itemStatus(item)
419 tag = "%s" % tag # Prevent weird error from lxml
420 value = "%s" % value # Prevent weird error from lxml
421 node = self.itemText(item.parent())
422 testbed = self.itemText(item.parent().parent())
423 if status == tag_status['add']:
424 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
425 if node.startsWith(default_tags):
426 rspec.add_default_sliver_attribute(tag, value, testbed)
428 rspec.add_sliver_attribute(node, tag, value, testbed)
430 elif status == tag_status['remove']:
431 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
432 if node.startsWith(default_tags):
433 rspec.remove_default_sliver_attribute(tag, value, testbed)
435 rspec.remove_sliver_attribute(node, tag, value, testbed)
438 children = item.rowCount()
439 for row in range(0, children):
440 status = self.process_subtree(rspec, item.child(row), depth + 1)
441 change = change or status
446 if self.checkRunningProcess():
449 rspec = self.readSliceRSpec()
450 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
453 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
456 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
457 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
459 self.process.applyRSpec(rspec)
460 self.setStatus("Sending slice data (RSpec). This will take some time...")
463 dlg = RenewWindow(parent=self)
467 if not config.getSlice():
468 self.setStatus("<font color='red'>Slice not set yet!</font>")
471 if self.process.isRunning():
472 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
475 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
476 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
478 self.process.getRSpecFromSM()
479 self.setStatus("Refreshing slice data. This will take some time...")
481 def updateView(self):
482 global already_in_nodes
483 already_in_nodes = []
484 self.network_names = []
485 self.nodeModel.clear()
487 rspec = self.readSliceRSpec()
491 rootItem = self.nodeModel.invisibleRootItem()
492 #networks = sorted(rspec.get_network_list())
493 networks = rspec.get_networks()
494 for network in networks:
495 self.network_names.append(network)
497 #all_nodes = rspec.get_node_list(network)
498 #sliver_nodes = rspec.get_sliver_list(network)
499 all_nodes = rspec.get_nodes(network)
500 sliver_nodes = rspec.get_nodes_with_slivers(network)
501 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
503 networkItem = QStandardItem(QString(network))
504 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
505 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
507 already_in_nodes += sliver_nodes
509 # Add default slice tags
510 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
511 statusItem = QStandardItem(QString(""))
512 nodeStatus = QStandardItem(QString(""))
513 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
514 attrs = rspec.get_default_sliver_attributes(network)
515 for (name, value) in attrs:
516 tagstring = QString("%s: %s" % (name, value))
517 tagItem = QStandardItem(tagstring)
518 status = QStandardItem(QString(tag_status['in']))
519 nodeStatus = QStandardItem(QString(""))
520 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
522 for node in sliver_nodes:
523 nodeItem = QStandardItem(QString(node))
524 statusItem = QStandardItem(QString(node_status['in']))
525 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
526 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
528 attrs = rspec.get_sliver_attributes(node, network)
529 for (name, value) in attrs:
530 tagstring = QString("%s: %s" % (name, value))
531 tagItem = QStandardItem(tagstring)
532 statusItem = QStandardItem(QString(tag_status['in']))
533 nodeStatus = QStandardItem(QString(""))
534 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
536 for node in available_nodes:
537 nodeItem = QStandardItem(QString(node))
538 statusItem = QStandardItem(QString(node_status['out']))
539 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
540 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
542 self.filterModel.setSourceModel(self.nodeModel)
543 self.filterModel.setDynamicSortFilter(True)
545 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
546 self.nodeModel.setHorizontalHeaderLabels(headers)
548 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
549 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
550 self.nodeView.setModel(self.filterModel)
551 self.nodeView.hideColumn(KIND_COLUMN)
552 self.nodeView.expandAll()
553 self.nodeView.resizeColumnToContents(0)
554 self.nodeView.collapseAll()
556 def updateSliceName(self):
557 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
559 def nodeSelectionChanged(self, hostname):
560 self.parent().nodeSelectionChanged(hostname)
562 class MainScreen(SfaScreen):
563 def __init__(self, parent):
564 SfaScreen.__init__(self, parent)
566 slice = SliceWidget(self)
567 self.init(slice, "Nodes", "OneLab SFA crawler")
569 def rspecUpdated(self):
570 self.mainwin.rspecWindow.updateView()
572 def configurationChanged(self):
573 self.widget.updateSliceName()
574 self.widget.updateView()
575 self.mainwin.rspecWindow.updateView()
577 def nodeSelectionChanged(self, hostname):
578 self.mainwin.nodeSelectionChanged(hostname)