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 keyPressEvent(self, event):
58 if (event.key() == Qt.Key_Space):
59 self.toggleSelection()
61 QTreeView.keyPressEvent(self, event)
63 def mouseDoubleClickEvent(self, event):
64 self.toggleSelection()
66 def toggleSelection(self):
67 index = self.currentIndex()
69 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
70 status_data = status_index.data().toString()
71 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
72 node_data = node_index.data().toString()
74 if itemType(node_index) == "tag":
75 data = node_index.data().toString()
76 tagname, value = data.split(": ")
77 if tagname not in settable_tags:
79 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
81 if status_data == tag_status['in']:
82 model.setData(status_index, QString(tag_status['remove']))
83 elif status_data == tag_status['add']:
84 model.setData(status_index, QString(tag_status['out']))
85 elif status_data == tag_status['remove']:
86 model.setData(status_index, QString(tag_status['in']))
87 else: model.setData(status_index, QString(node_status['out']))
90 if status_data == node_status['in']:
91 model.setData(status_index, QString(node_status['remove']))
92 elif status_data == node_status['out']:
93 model.setData(status_index, QString(node_status['add']))
94 elif status_data in (node_status['add'], node_status['remove']):
95 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
96 else: model.setData(status_index, QString(node_status['out']))
98 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
100 def mousePressEvent(self, event):
101 QTreeView.mousePressEvent(self, event)
102 if event.button() == Qt.LeftButton:
106 index = self.currentIndex()
107 model = index.model()
108 status_index = model.index(index.row(), 1, index.parent())
109 status_data = status_index.data().toString()
110 node_index = model.index(index.row(), 0, index.parent())
111 node_data = node_index.data().toString()
113 if itemType(node_index) == "node":
115 if status_data in (node_status['in'], node_status['add'], ""):
116 # Pop up a dialog box for adding a new attribute
117 tagname, ok = QInputDialog.getItem(self, "Add tag",
118 "Tag name:", settable_tags)
120 value, ok = QInputDialog.getText(self, "Add tag",
121 "Value for tag '%s'" % tagname)
123 # Add a new row to the model for the tag
125 # For testing with the QStandardItemModel
126 #nodeItem = model.itemFromIndex(index)
127 #tagstring = QString("%s: %s" % (tagname, value))
128 #tagItem = QStandardItem(tagstring)
129 #status = QStandardItem(QString(tag_status['add']))
130 #nodeItem.appendRow([tagItem, status])
132 # We're using the QSortFilterProxyModel here
133 src_index = model.mapToSource(index)
134 src_model = src_index.model()
135 nodeItem = src_model.itemFromIndex(src_index)
136 tagstring = QString("%s: %s" % (tagname, value))
137 tagItem = QStandardItem(tagstring)
138 status = QStandardItem(QString(tag_status['add']))
139 nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
141 elif status_data in (node_status['out'], node_status['remove']):
142 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
145 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
147 def currentChanged(self, current, previous):
148 model = current.model()
149 node_index = model.index(current.row(), 0, current.parent())
150 node_data = node_index.data().toString()
151 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
155 class NodeNameDelegate(QStyledItemDelegate):
156 def __init__(self, parent):
157 QStyledItemDelegate.__init__(self, parent)
159 def displayText(self, value, locale):
160 data = str(QStyledItemDelegate.displayText(self, value, locale))
161 if (len(data)>NAME_MAX_LEN):
162 data = data[:(NAME_MAX_LEN-3)] + "..."
165 def paint(self, painter, option, index):
166 model = index.model()
167 data = str(self.displayText(index.data(), QLocale()))
168 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
169 status_data = status_index.data().toString()
171 fm = QFontMetrics(option.font)
172 rect = QRect(option.rect)
174 rect.setHeight(rect.height() - 2)
175 rect.setWidth(fm.width(QString(data)) + 6)
176 rect.setX(rect.x() + 5)
177 rect.setY(rect.y() - 1)
179 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
181 path = QPainterPath()
182 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
185 painter.setRenderHint(QPainter.Antialiasing)
187 if option.state & QStyle.State_Selected:
188 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
190 if itemType(index) == "node":
191 if status_data == node_status['in']: # already in the slice
192 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
193 painter.setPen(QColor.fromRgb(0, 0, 0))
194 painter.drawText(rect, 0, QString(data))
196 elif status_data == node_status['add']: # newly added to the slice
197 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
198 painter.setPen(QColor.fromRgb(0, 0, 0))
199 painter.drawText(rect, 0, QString(data))
201 elif status_data == node_status['remove']: # removed from the slice
202 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
203 painter.setPen(QColor.fromRgb(0, 0, 0))
204 painter.drawText(rect, 0, QString(data))
207 painter.setPen(QColor.fromRgb(0, 0, 0))
208 painter.drawText(rect, 0, QString(data))
211 if status_data == tag_status['in']: # already in the slice
212 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
213 painter.setPen(QColor.fromRgb(0, 0, 0))
214 painter.drawText(rect, 0, QString(data))
216 elif status_data == tag_status['add']: # newly added to the slice
217 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
218 painter.setPen(QColor.fromRgb(0, 0, 0))
219 painter.drawText(rect, 0, QString(data))
221 elif status_data == tag_status['remove']: # removed from the slice
222 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
223 painter.setPen(QColor.fromRgb(0, 0, 0))
224 painter.drawText(rect, 0, QString(data))
227 painter.setPen(QColor.fromRgb(0, 0, 0))
228 painter.drawText(rect, 0, QString(data))
232 class NodeStatusDelegate(QStyledItemDelegate):
233 def __init__(self, parent):
234 QStyledItemDelegate.__init__(self, parent)
236 def paint(self, painter, option, index):
237 model = index.model()
238 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
239 nodestatus_data = nodestatus_index.data().toString()
241 fm = QFontMetrics(option.font)
242 rect = QRect(option.rect)
244 data = index.data().toString()
245 rect.setHeight(rect.height() - 2)
246 rect.setWidth(fm.width(QString(data)) + 6)
247 rect.setX(rect.x() + 5)
248 rect.setY(rect.y() - 1)
250 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
252 path = QPainterPath()
253 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
256 painter.setRenderHint(QPainter.Antialiasing)
258 if option.state & QStyle.State_Selected:
259 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
261 if (nodestatus_data == ""):
262 painter.setPen(QColor.fromRgb(0, 0, 0))
263 painter.drawText(rect, 0, QString(data))
264 elif (nodestatus_data == "boot"):
265 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
266 painter.setPen(QColor.fromRgb(0, 0, 0))
267 painter.drawText(rect, 0, QString(data))
269 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
270 painter.setPen(QColor.fromRgb(0, 0, 0))
271 painter.drawText(rect, 0, QString(data))
275 class NodeFilterProxyModel(QSortFilterProxyModel):
276 def __init__(self, parent=None):
277 QSortFilterProxyModel.__init__(self, parent)
278 self.hostname_filter_regex = None
279 self.nodestatus_filter = None
281 def setHostNameFilter(self, hostname):
282 self.hostname_filter_regex = QRegExp(hostname)
283 self.invalidateFilter()
285 def setNodeStatusFilter(self, status):
286 if (status == "all"):
287 self.nodestatus_filter = None
289 self.nodestatus_filter = status
290 self.invalidateFilter()
292 def filterAcceptsRow(self, sourceRow, source_parent):
293 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
294 if (kind_data == "node"):
295 if self.hostname_filter_regex:
296 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
297 if (self.hostname_filter_regex.indexIn(name_data) < 0):
299 if self.nodestatus_filter:
300 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
301 if (nodestatus_data != self.nodestatus_filter):
305 class SliceWidget(QWidget):
306 def __init__(self, parent):
307 QWidget.__init__(self, parent)
309 self.network_names = []
310 self.process = SfiProcess(self)
312 self.slicename = QLabel("", self)
313 self.updateSliceName()
314 self.slicename.setScaledContents(False)
315 filterlabel = QLabel ("Filter: ", self)
316 filterbox = QComboBox(self)
317 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
318 searchlabel = QLabel ("Search: ", self)
319 searchlabel.setScaledContents(False)
320 searchbox = QLineEdit(self)
321 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
323 toplayout = QHBoxLayout()
324 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
325 toplayout.addStretch()
326 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
327 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
328 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
329 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
331 self.nodeView = NodeView(self)
332 self.nodeModel = QStandardItemModel(0, 4, self)
333 self.filterModel = NodeFilterProxyModel(self)
335 self.nodeNameDelegate = NodeNameDelegate(self)
336 self.nodeStatusDelegate = NodeStatusDelegate(self)
338 refresh = QPushButton("Refresh Slice Data", self)
339 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
340 renew = QPushButton("Renew Slice", self)
341 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
342 submit = QPushButton("Submit", self)
343 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
345 bottomlayout = QHBoxLayout()
346 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
347 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
348 bottomlayout.addStretch()
349 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
351 layout = QVBoxLayout()
352 layout.addLayout(toplayout)
353 layout.addWidget(self.nodeView)
354 layout.addLayout(bottomlayout)
355 self.setLayout(layout)
356 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
358 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
359 self.connect(renew, SIGNAL('clicked()'), self.renew)
360 self.connect(submit, SIGNAL('clicked()'), self.submit)
361 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
362 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
363 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
364 self.nodeSelectionChanged)
368 def submitFinished(self):
369 faultString = self.process.getFaultString()
371 self.setStatus("<font color='green'>Slice data submitted.</font>")
373 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
375 # no need to do that anymore
376 # QTimer.singleShot(1000, self.refresh)
378 self.parent().signalAll("rspecUpdated")
380 def refreshFinished(self):
381 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
383 self.parent().signalAll("rspecUpdated")
385 def readSliceRSpec(self):
386 rspec_file = config.getSliceRSpecFile()
387 if os.path.exists(rspec_file):
388 xml = open(rspec_file).read()
389 return parse_rspec(xml)
392 def setStatus(self, msg, timeout=None):
393 self.parent().setStatus(msg, timeout)
395 def checkRunningProcess(self):
396 if self.process.isRunning():
397 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
401 def search(self, search_string):
402 self.filterModel.setHostNameFilter(str(search_string))
404 def filter(self, filter_string):
405 self.filterModel.setNodeStatusFilter(str(filter_string))
407 def itemStatus(self, item):
408 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
409 return statusItem.data(Qt.DisplayRole).toString()
411 def itemText(self, item):
412 return item.data(Qt.DisplayRole).toString()
414 # Recursively walk the tree, making changes to the RSpec
415 def process_subtree(self, rspec, item, depth = 0):
417 model = self.nodeModel
421 elif depth == 2: # Hostname
422 hostname = self.itemText(item)
423 testbed = self.itemText(item.parent())
424 status = self.itemStatus(item)
425 if status == node_status['add']:
426 print "Add hostname: %s" % hostname
427 rspec.add_slivers(str(hostname), testbed)
429 elif status == node_status['remove']:
430 print "Remove hostname: %s" % hostname
431 rspec.remove_slivers(str(hostname), testbed)
433 elif depth == 3: # Tag
434 tag, value = self.itemText(item).split(": ")
435 status = self.itemStatus(item)
436 tag = "%s" % tag # Prevent weird error from lxml
437 value = "%s" % value # Prevent weird error from lxml
438 node = self.itemText(item.parent())
439 testbed = self.itemText(item.parent().parent())
440 if status == tag_status['add']:
441 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
442 if node.startsWith(default_tags):
443 rspec.add_default_sliver_attribute(tag, value, testbed)
445 rspec.add_sliver_attribute(node, tag, value, testbed)
447 elif status == tag_status['remove']:
448 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
449 if node.startsWith(default_tags):
450 rspec.remove_default_sliver_attribute(tag, value, testbed)
452 rspec.remove_sliver_attribute(node, tag, value, testbed)
455 children = item.rowCount()
456 for row in range(0, children):
457 status = self.process_subtree(rspec, item.child(row), depth + 1)
458 change = change or status
463 if self.checkRunningProcess():
466 rspec = self.readSliceRSpec()
467 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
470 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
473 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
474 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
476 self.process.applyRSpec(rspec)
477 self.setStatus("Sending slice data (RSpec). This will take some time...")
480 dlg = RenewWindow(parent=self)
484 if not config.getSlice():
485 self.setStatus("<font color='red'>Slice not set yet!</font>")
488 if self.process.isRunning():
489 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
492 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
493 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
495 self.process.retrieveRspec()
496 self.setStatus("Refreshing slice data. This will take some time...")
498 def updateView(self):
499 global already_in_nodes
500 already_in_nodes = []
501 self.network_names = []
502 self.nodeModel.clear()
504 rspec = self.readSliceRSpec()
508 rootItem = self.nodeModel.invisibleRootItem()
509 #networks = sorted(rspec.get_network_list())
510 networks = rspec.get_networks()
511 for network in networks:
512 self.network_names.append(network)
514 #all_nodes = rspec.get_node_list(network)
515 #sliver_nodes = rspec.get_sliver_list(network)
516 all_nodes = rspec.get_nodes(network)
517 sliver_nodes = rspec.get_nodes_with_slivers(network)
518 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
520 networkItem = QStandardItem(QString(network))
521 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
522 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
524 already_in_nodes += sliver_nodes
526 # Add default slice tags
527 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
528 statusItem = QStandardItem(QString(""))
529 nodeStatus = QStandardItem(QString(""))
530 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
531 attrs = rspec.get_default_sliver_attributes(network)
532 for (name, value) in attrs:
533 tagstring = QString("%s: %s" % (name, value))
534 tagItem = QStandardItem(tagstring)
535 status = QStandardItem(QString(tag_status['in']))
536 nodeStatus = QStandardItem(QString(""))
537 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
539 for node in sliver_nodes:
540 nodeItem = QStandardItem(QString(node))
541 statusItem = QStandardItem(QString(node_status['in']))
542 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
543 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
545 attrs = rspec.get_sliver_attributes(node, network)
546 for (name, value) in attrs:
547 tagstring = QString("%s: %s" % (name, value))
548 tagItem = QStandardItem(tagstring)
549 statusItem = QStandardItem(QString(tag_status['in']))
550 nodeStatus = QStandardItem(QString(""))
551 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
553 for node in available_nodes:
554 nodeItem = QStandardItem(QString(node))
555 statusItem = QStandardItem(QString(node_status['out']))
556 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
557 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
559 self.filterModel.setSourceModel(self.nodeModel)
560 self.filterModel.setDynamicSortFilter(True)
562 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
563 self.nodeModel.setHorizontalHeaderLabels(headers)
565 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
566 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
567 self.nodeView.setModel(self.filterModel)
568 self.nodeView.hideColumn(KIND_COLUMN)
569 self.nodeView.expandAll()
570 self.nodeView.resizeColumnToContents(0)
571 self.nodeView.collapseAll()
573 def updateSliceName(self):
574 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
576 def nodeSelectionChanged(self, hostname):
577 self.parent().nodeSelectionChanged(hostname)
579 class MainScreen(SfaScreen):
580 def __init__(self, parent):
581 SfaScreen.__init__(self, parent)
583 slice = SliceWidget(self)
584 self.init(slice, "Nodes", "OneLab SFA crawler")
586 def rspecUpdated(self):
587 self.mainwin.rspecWindow.updateView()
589 def configurationChanged(self):
590 self.widget.updateSliceName()
591 self.widget.updateView()
592 self.mainwin.rspecWindow.updateView()
594 def nodeSelectionChanged(self, hostname):
595 self.mainwin.nodeSelectionChanged(hostname)