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 # no need to do that anymore
362 # QTimer.singleShot(1000, self.refresh)
364 self.parent().signalAll("rspecUpdated")
366 def refreshFinished(self):
367 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
369 self.parent().signalAll("rspecUpdated")
371 def readSliceRSpec(self):
372 rspec_file = config.getSliceRSpecFile()
373 if os.path.exists(rspec_file):
374 xml = open(rspec_file).read()
375 return parse_rspec(xml)
378 def setStatus(self, msg, timeout=None):
379 self.parent().setStatus(msg, timeout)
381 def checkRunningProcess(self):
382 if self.process.isRunning():
383 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
387 def search(self, search_string):
388 self.filterModel.setHostNameFilter(str(search_string))
390 def filter(self, filter_string):
391 self.filterModel.setNodeStatusFilter(str(filter_string))
393 def itemStatus(self, item):
394 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
395 return statusItem.data(Qt.DisplayRole).toString()
397 def itemText(self, item):
398 return item.data(Qt.DisplayRole).toString()
400 # Recursively walk the tree, making changes to the RSpec
401 def process_subtree(self, rspec, item, depth = 0):
403 model = self.nodeModel
407 elif depth == 2: # Hostname
408 hostname = self.itemText(item)
409 testbed = self.itemText(item.parent())
410 status = self.itemStatus(item)
411 if status == node_status['add']:
412 print "Add hostname: %s" % hostname
413 rspec.add_slivers(str(hostname), testbed)
415 elif status == node_status['remove']:
416 print "Remove hostname: %s" % hostname
417 rspec.remove_slivers(str(hostname), testbed)
419 elif depth == 3: # Tag
420 tag, value = self.itemText(item).split(": ")
421 status = self.itemStatus(item)
422 tag = "%s" % tag # Prevent weird error from lxml
423 value = "%s" % value # Prevent weird error from lxml
424 node = self.itemText(item.parent())
425 testbed = self.itemText(item.parent().parent())
426 if status == tag_status['add']:
427 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
428 if node.startsWith(default_tags):
429 rspec.add_default_sliver_attribute(tag, value, testbed)
431 rspec.add_sliver_attribute(node, tag, value, testbed)
433 elif status == tag_status['remove']:
434 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
435 if node.startsWith(default_tags):
436 rspec.remove_default_sliver_attribute(tag, value, testbed)
438 rspec.remove_sliver_attribute(node, tag, value, testbed)
441 children = item.rowCount()
442 for row in range(0, children):
443 status = self.process_subtree(rspec, item.child(row), depth + 1)
444 change = change or status
449 if self.checkRunningProcess():
452 rspec = self.readSliceRSpec()
453 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
456 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
459 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
460 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
462 self.process.applyRSpec(rspec)
463 self.setStatus("Sending slice data (RSpec). This will take some time...")
466 dlg = RenewWindow(parent=self)
470 if not config.getSlice():
471 self.setStatus("<font color='red'>Slice not set yet!</font>")
474 if self.process.isRunning():
475 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
478 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
479 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
481 self.process.retrieveRspec()
482 self.setStatus("Refreshing slice data. This will take some time...")
484 def updateView(self):
485 global already_in_nodes
486 already_in_nodes = []
487 self.network_names = []
488 self.nodeModel.clear()
490 rspec = self.readSliceRSpec()
494 rootItem = self.nodeModel.invisibleRootItem()
495 #networks = sorted(rspec.get_network_list())
496 networks = rspec.get_networks()
497 for network in networks:
498 self.network_names.append(network)
500 #all_nodes = rspec.get_node_list(network)
501 #sliver_nodes = rspec.get_sliver_list(network)
502 all_nodes = rspec.get_nodes(network)
503 sliver_nodes = rspec.get_nodes_with_slivers(network)
504 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
506 networkItem = QStandardItem(QString(network))
507 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
508 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
510 already_in_nodes += sliver_nodes
512 # Add default slice tags
513 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
514 statusItem = QStandardItem(QString(""))
515 nodeStatus = QStandardItem(QString(""))
516 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
517 attrs = rspec.get_default_sliver_attributes(network)
518 for (name, value) in attrs:
519 tagstring = QString("%s: %s" % (name, value))
520 tagItem = QStandardItem(tagstring)
521 status = QStandardItem(QString(tag_status['in']))
522 nodeStatus = QStandardItem(QString(""))
523 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
525 for node in sliver_nodes:
526 nodeItem = QStandardItem(QString(node))
527 statusItem = QStandardItem(QString(node_status['in']))
528 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
529 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
531 attrs = rspec.get_sliver_attributes(node, network)
532 for (name, value) in attrs:
533 tagstring = QString("%s: %s" % (name, value))
534 tagItem = QStandardItem(tagstring)
535 statusItem = QStandardItem(QString(tag_status['in']))
536 nodeStatus = QStandardItem(QString(""))
537 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
539 for node in available_nodes:
540 nodeItem = QStandardItem(QString(node))
541 statusItem = QStandardItem(QString(node_status['out']))
542 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
543 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
545 self.filterModel.setSourceModel(self.nodeModel)
546 self.filterModel.setDynamicSortFilter(True)
548 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
549 self.nodeModel.setHorizontalHeaderLabels(headers)
551 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
552 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
553 self.nodeView.setModel(self.filterModel)
554 self.nodeView.hideColumn(KIND_COLUMN)
555 self.nodeView.expandAll()
556 self.nodeView.resizeColumnToContents(0)
557 self.nodeView.collapseAll()
559 def updateSliceName(self):
560 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
562 def nodeSelectionChanged(self, hostname):
563 self.parent().nodeSelectionChanged(hostname)
565 class MainScreen(SfaScreen):
566 def __init__(self, parent):
567 SfaScreen.__init__(self, parent)
569 slice = SliceWidget(self)
570 self.init(slice, "Nodes", "OneLab SFA crawler")
572 def rspecUpdated(self):
573 self.mainwin.rspecWindow.updateView()
575 def configurationChanged(self):
576 self.widget.updateSliceName()
577 self.widget.updateView()
578 self.mainwin.rspecWindow.updateView()
580 def nodeSelectionChanged(self, hostname):
581 self.mainwin.nodeSelectionChanged(hostname)