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 faultString = self.process.getFaultString()
362 self.setStatus("<font color='green'>Slice data submitted.</font>")
364 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
366 # no need to do that anymore
367 # QTimer.singleShot(1000, self.refresh)
369 self.parent().signalAll("rspecUpdated")
371 def refreshFinished(self):
372 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
374 self.parent().signalAll("rspecUpdated")
376 def readSliceRSpec(self):
377 rspec_file = config.getSliceRSpecFile()
378 if os.path.exists(rspec_file):
379 xml = open(rspec_file).read()
380 return parse_rspec(xml)
383 def setStatus(self, msg, timeout=None):
384 self.parent().setStatus(msg, timeout)
386 def checkRunningProcess(self):
387 if self.process.isRunning():
388 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
392 def search(self, search_string):
393 self.filterModel.setHostNameFilter(str(search_string))
395 def filter(self, filter_string):
396 self.filterModel.setNodeStatusFilter(str(filter_string))
398 def itemStatus(self, item):
399 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
400 return statusItem.data(Qt.DisplayRole).toString()
402 def itemText(self, item):
403 return item.data(Qt.DisplayRole).toString()
405 # Recursively walk the tree, making changes to the RSpec
406 def process_subtree(self, rspec, item, depth = 0):
408 model = self.nodeModel
412 elif depth == 2: # Hostname
413 hostname = self.itemText(item)
414 testbed = self.itemText(item.parent())
415 status = self.itemStatus(item)
416 if status == node_status['add']:
417 print "Add hostname: %s" % hostname
418 rspec.add_slivers(str(hostname), testbed)
420 elif status == node_status['remove']:
421 print "Remove hostname: %s" % hostname
422 rspec.remove_slivers(str(hostname), testbed)
424 elif depth == 3: # Tag
425 tag, value = self.itemText(item).split(": ")
426 status = self.itemStatus(item)
427 tag = "%s" % tag # Prevent weird error from lxml
428 value = "%s" % value # Prevent weird error from lxml
429 node = self.itemText(item.parent())
430 testbed = self.itemText(item.parent().parent())
431 if status == tag_status['add']:
432 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
433 if node.startsWith(default_tags):
434 rspec.add_default_sliver_attribute(tag, value, testbed)
436 rspec.add_sliver_attribute(node, tag, value, testbed)
438 elif status == tag_status['remove']:
439 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
440 if node.startsWith(default_tags):
441 rspec.remove_default_sliver_attribute(tag, value, testbed)
443 rspec.remove_sliver_attribute(node, tag, value, testbed)
446 children = item.rowCount()
447 for row in range(0, children):
448 status = self.process_subtree(rspec, item.child(row), depth + 1)
449 change = change or status
454 if self.checkRunningProcess():
457 rspec = self.readSliceRSpec()
458 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
461 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
464 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
465 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
467 self.process.applyRSpec(rspec)
468 self.setStatus("Sending slice data (RSpec). This will take some time...")
471 dlg = RenewWindow(parent=self)
475 if not config.getSlice():
476 self.setStatus("<font color='red'>Slice not set yet!</font>")
479 if self.process.isRunning():
480 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
483 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
484 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
486 self.process.retrieveRspec()
487 self.setStatus("Refreshing slice data. This will take some time...")
489 def updateView(self):
490 global already_in_nodes
491 already_in_nodes = []
492 self.network_names = []
493 self.nodeModel.clear()
495 rspec = self.readSliceRSpec()
499 rootItem = self.nodeModel.invisibleRootItem()
500 #networks = sorted(rspec.get_network_list())
501 networks = rspec.get_networks()
502 for network in networks:
503 self.network_names.append(network)
505 #all_nodes = rspec.get_node_list(network)
506 #sliver_nodes = rspec.get_sliver_list(network)
507 all_nodes = rspec.get_nodes(network)
508 sliver_nodes = rspec.get_nodes_with_slivers(network)
509 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
511 networkItem = QStandardItem(QString(network))
512 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
513 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
515 already_in_nodes += sliver_nodes
517 # Add default slice tags
518 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
519 statusItem = QStandardItem(QString(""))
520 nodeStatus = QStandardItem(QString(""))
521 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
522 attrs = rspec.get_default_sliver_attributes(network)
523 for (name, value) in attrs:
524 tagstring = QString("%s: %s" % (name, value))
525 tagItem = QStandardItem(tagstring)
526 status = QStandardItem(QString(tag_status['in']))
527 nodeStatus = QStandardItem(QString(""))
528 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
530 for node in sliver_nodes:
531 nodeItem = QStandardItem(QString(node))
532 statusItem = QStandardItem(QString(node_status['in']))
533 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
534 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
536 attrs = rspec.get_sliver_attributes(node, network)
537 for (name, value) in attrs:
538 tagstring = QString("%s: %s" % (name, value))
539 tagItem = QStandardItem(tagstring)
540 statusItem = QStandardItem(QString(tag_status['in']))
541 nodeStatus = QStandardItem(QString(""))
542 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
544 for node in available_nodes:
545 nodeItem = QStandardItem(QString(node))
546 statusItem = QStandardItem(QString(node_status['out']))
547 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
548 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
550 self.filterModel.setSourceModel(self.nodeModel)
551 self.filterModel.setDynamicSortFilter(True)
553 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
554 self.nodeModel.setHorizontalHeaderLabels(headers)
556 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
557 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
558 self.nodeView.setModel(self.filterModel)
559 self.nodeView.hideColumn(KIND_COLUMN)
560 self.nodeView.expandAll()
561 self.nodeView.resizeColumnToContents(0)
562 self.nodeView.collapseAll()
564 def updateSliceName(self):
565 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
567 def nodeSelectionChanged(self, hostname):
568 self.parent().nodeSelectionChanged(hostname)
570 class MainScreen(SfaScreen):
571 def __init__(self, parent):
572 SfaScreen.__init__(self, parent)
574 slice = SliceWidget(self)
575 self.init(slice, "Nodes", "OneLab SFA crawler")
577 def rspecUpdated(self):
578 self.mainwin.rspecWindow.updateView()
580 def configurationChanged(self):
581 self.widget.updateSliceName()
582 self.widget.updateView()
583 self.mainwin.rspecWindow.updateView()
585 def nodeSelectionChanged(self, hostname):
586 self.mainwin.nodeSelectionChanged(hostname)