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 def mouseDoubleClickEvent(self, event):
62 self.toggleSelection()
64 def toggleSelection(self):
65 index = self.currentIndex()
67 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
68 status_data = status_index.data().toString()
69 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
70 node_data = node_index.data().toString()
72 if itemType(node_index) == "tag":
73 data = node_index.data().toString()
74 tagname, value = data.split(": ")
75 if tagname not in settable_tags:
77 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
79 if status_data == tag_status['in']:
80 model.setData(status_index, QString(tag_status['remove']))
81 elif status_data == tag_status['add']:
82 model.setData(status_index, QString(tag_status['out']))
83 elif status_data == tag_status['remove']:
84 model.setData(status_index, QString(tag_status['in']))
85 else: model.setData(status_index, QString(node_status['out']))
88 if status_data == node_status['in']:
89 model.setData(status_index, QString(node_status['remove']))
90 elif status_data == node_status['out']:
91 model.setData(status_index, QString(node_status['add']))
92 elif status_data in (node_status['add'], node_status['remove']):
93 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
94 else: model.setData(status_index, QString(node_status['out']))
96 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
98 def mousePressEvent(self, event):
99 QTreeView.mousePressEvent(self, event)
100 if event.button() == Qt.LeftButton:
104 index = self.currentIndex()
105 model = index.model()
106 status_index = model.index(index.row(), 1, index.parent())
107 status_data = status_index.data().toString()
108 node_index = model.index(index.row(), 0, index.parent())
109 node_data = node_index.data().toString()
111 if itemType(node_index) == "node":
113 if status_data in (node_status['in'], node_status['add'], ""):
114 # Pop up a dialog box for adding a new attribute
115 tagname, ok = QInputDialog.getItem(self, "Add tag",
116 "Tag name:", settable_tags)
118 value, ok = QInputDialog.getText(self, "Add tag",
119 "Value for tag '%s'" % tagname)
121 # Add a new row to the model for the tag
123 # For testing with the QStandardItemModel
124 #nodeItem = model.itemFromIndex(index)
125 #tagstring = QString("%s: %s" % (tagname, value))
126 #tagItem = QStandardItem(tagstring)
127 #status = QStandardItem(QString(tag_status['add']))
128 #nodeItem.appendRow([tagItem, status])
130 # We're using the QSortFilterProxyModel here
131 src_index = model.mapToSource(index)
132 src_model = src_index.model()
133 nodeItem = src_model.itemFromIndex(src_index)
134 tagstring = QString("%s: %s" % (tagname, value))
135 tagItem = QStandardItem(tagstring)
136 status = QStandardItem(QString(tag_status['add']))
137 nodeItem.appendRow([tagItem, QStandardItem(QString("")), status])
139 elif status_data in (node_status['out'], node_status['remove']):
140 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
143 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
145 def currentChanged(self, current, previous):
146 model = current.model()
147 node_index = model.index(current.row(), 0, current.parent())
148 node_data = node_index.data().toString()
149 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
153 class NodeNameDelegate(QStyledItemDelegate):
154 def __init__(self, parent):
155 QStyledItemDelegate.__init__(self, parent)
157 def displayText(self, value, locale):
158 data = str(QStyledItemDelegate.displayText(self, value, locale))
159 if (len(data)>NAME_MAX_LEN):
160 data = data[:(NAME_MAX_LEN-3)] + "..."
163 def paint(self, painter, option, index):
164 model = index.model()
165 data = str(self.displayText(index.data(), QLocale()))
166 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
167 status_data = status_index.data().toString()
169 fm = QFontMetrics(option.font)
170 rect = QRect(option.rect)
172 rect.setHeight(rect.height() - 2)
173 rect.setWidth(fm.width(QString(data)) + 6)
174 rect.setX(rect.x() + 5)
175 rect.setY(rect.y() - 1)
177 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
179 path = QPainterPath()
180 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
183 painter.setRenderHint(QPainter.Antialiasing)
185 if option.state & QStyle.State_Selected:
186 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
188 if itemType(index) == "node":
189 if status_data == node_status['in']: # already in the slice
190 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
191 painter.setPen(QColor.fromRgb(0, 0, 0))
192 painter.drawText(rect, 0, QString(data))
194 elif status_data == node_status['add']: # newly added to the slice
195 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
196 painter.setPen(QColor.fromRgb(0, 0, 0))
197 painter.drawText(rect, 0, QString(data))
199 elif status_data == node_status['remove']: # removed from the slice
200 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
201 painter.setPen(QColor.fromRgb(0, 0, 0))
202 painter.drawText(rect, 0, QString(data))
205 painter.setPen(QColor.fromRgb(0, 0, 0))
206 painter.drawText(rect, 0, QString(data))
209 if status_data == tag_status['in']: # already in the slice
210 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
211 painter.setPen(QColor.fromRgb(0, 0, 0))
212 painter.drawText(rect, 0, QString(data))
214 elif status_data == tag_status['add']: # newly added to the slice
215 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
216 painter.setPen(QColor.fromRgb(0, 0, 0))
217 painter.drawText(rect, 0, QString(data))
219 elif status_data == tag_status['remove']: # removed from the slice
220 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
221 painter.setPen(QColor.fromRgb(0, 0, 0))
222 painter.drawText(rect, 0, QString(data))
225 painter.setPen(QColor.fromRgb(0, 0, 0))
226 painter.drawText(rect, 0, QString(data))
230 class NodeStatusDelegate(QStyledItemDelegate):
231 def __init__(self, parent):
232 QStyledItemDelegate.__init__(self, parent)
234 def paint(self, painter, option, index):
235 model = index.model()
236 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
237 nodestatus_data = nodestatus_index.data().toString()
239 fm = QFontMetrics(option.font)
240 rect = QRect(option.rect)
242 data = index.data().toString()
243 rect.setHeight(rect.height() - 2)
244 rect.setWidth(fm.width(QString(data)) + 6)
245 rect.setX(rect.x() + 5)
246 rect.setY(rect.y() - 1)
248 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
250 path = QPainterPath()
251 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
254 painter.setRenderHint(QPainter.Antialiasing)
256 if option.state & QStyle.State_Selected:
257 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
259 if (nodestatus_data == ""):
260 painter.setPen(QColor.fromRgb(0, 0, 0))
261 painter.drawText(rect, 0, QString(data))
262 elif (nodestatus_data == "boot"):
263 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
264 painter.setPen(QColor.fromRgb(0, 0, 0))
265 painter.drawText(rect, 0, QString(data))
267 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
268 painter.setPen(QColor.fromRgb(0, 0, 0))
269 painter.drawText(rect, 0, QString(data))
273 class NodeFilterProxyModel(QSortFilterProxyModel):
274 def __init__(self, parent=None):
275 QSortFilterProxyModel.__init__(self, parent)
276 self.hostname_filter_regex = None
277 self.nodestatus_filter = None
279 def setHostNameFilter(self, hostname):
280 self.hostname_filter_regex = QRegExp(hostname)
281 self.invalidateFilter()
283 def setNodeStatusFilter(self, status):
284 if (status == "all"):
285 self.nodestatus_filter = None
287 self.nodestatus_filter = status
288 self.invalidateFilter()
290 def filterAcceptsRow(self, sourceRow, source_parent):
291 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
292 if (kind_data == "node"):
293 if self.hostname_filter_regex:
294 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
295 if (self.hostname_filter_regex.indexIn(name_data) < 0):
297 if self.nodestatus_filter:
298 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
299 if (nodestatus_data != self.nodestatus_filter):
303 class SliceWidget(QWidget):
304 def __init__(self, parent):
305 QWidget.__init__(self, parent)
307 self.network_names = []
308 self.process = SfiProcess(self)
310 self.slicename = QLabel("", self)
311 self.updateSliceName()
312 self.slicename.setScaledContents(False)
313 filterlabel = QLabel ("Filter: ", self)
314 filterbox = QComboBox(self)
315 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
316 searchlabel = QLabel ("Search: ", self)
317 searchlabel.setScaledContents(False)
318 searchbox = QLineEdit(self)
319 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
321 toplayout = QHBoxLayout()
322 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
323 toplayout.addStretch()
324 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
325 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
326 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
327 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
329 self.nodeView = NodeView(self)
330 self.nodeModel = QStandardItemModel(0, 4, self)
331 self.filterModel = NodeFilterProxyModel(self)
333 self.nodeNameDelegate = NodeNameDelegate(self)
334 self.nodeStatusDelegate = NodeStatusDelegate(self)
336 refresh = QPushButton("Refresh Slice Data", self)
337 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
338 renew = QPushButton("Renew Slice", self)
339 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
340 submit = QPushButton("Submit", self)
341 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
343 bottomlayout = QHBoxLayout()
344 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
345 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
346 bottomlayout.addStretch()
347 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
349 layout = QVBoxLayout()
350 layout.addLayout(toplayout)
351 layout.addWidget(self.nodeView)
352 layout.addLayout(bottomlayout)
353 self.setLayout(layout)
354 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
356 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
357 self.connect(renew, SIGNAL('clicked()'), self.renew)
358 self.connect(submit, SIGNAL('clicked()'), self.submit)
359 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
360 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
361 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
362 self.nodeSelectionChanged)
366 def submitFinished(self):
367 faultString = self.process.getFaultString()
369 self.setStatus("<font color='green'>Slice data submitted.</font>")
371 self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
373 # no need to do that anymore
374 # QTimer.singleShot(1000, self.refresh)
376 self.parent().signalAll("rspecUpdated")
378 def refreshFinished(self):
379 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
381 self.parent().signalAll("rspecUpdated")
383 def readSliceRSpec(self):
384 rspec_file = config.getSliceRSpecFile()
385 if os.path.exists(rspec_file):
386 xml = open(rspec_file).read()
387 return parse_rspec(xml)
390 def setStatus(self, msg, timeout=None):
391 self.parent().setStatus(msg, timeout)
393 def checkRunningProcess(self):
394 if self.process.isRunning():
395 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
399 def search(self, search_string):
400 self.filterModel.setHostNameFilter(str(search_string))
402 def filter(self, filter_string):
403 self.filterModel.setNodeStatusFilter(str(filter_string))
405 def itemStatus(self, item):
406 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
407 return statusItem.data(Qt.DisplayRole).toString()
409 def itemText(self, item):
410 return item.data(Qt.DisplayRole).toString()
412 # Recursively walk the tree, making changes to the RSpec
413 def process_subtree(self, rspec, item, depth = 0):
415 model = self.nodeModel
419 elif depth == 2: # Hostname
420 hostname = self.itemText(item)
421 testbed = self.itemText(item.parent())
422 status = self.itemStatus(item)
423 if status == node_status['add']:
424 print "Add hostname: %s" % hostname
425 rspec.add_slivers(str(hostname), testbed)
427 elif status == node_status['remove']:
428 print "Remove hostname: %s" % hostname
429 rspec.remove_slivers(str(hostname), testbed)
431 elif depth == 3: # Tag
432 tag, value = self.itemText(item).split(": ")
433 status = self.itemStatus(item)
434 tag = "%s" % tag # Prevent weird error from lxml
435 value = "%s" % value # Prevent weird error from lxml
436 node = self.itemText(item.parent())
437 testbed = self.itemText(item.parent().parent())
438 if status == tag_status['add']:
439 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
440 if node.startsWith(default_tags):
441 rspec.add_default_sliver_attribute(tag, value, testbed)
443 rspec.add_sliver_attribute(node, tag, value, testbed)
445 elif status == tag_status['remove']:
446 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
447 if node.startsWith(default_tags):
448 rspec.remove_default_sliver_attribute(tag, value, testbed)
450 rspec.remove_sliver_attribute(node, tag, value, testbed)
453 children = item.rowCount()
454 for row in range(0, children):
455 status = self.process_subtree(rspec, item.child(row), depth + 1)
456 change = change or status
461 if self.checkRunningProcess():
464 rspec = self.readSliceRSpec()
465 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
468 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
471 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
472 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
474 self.process.applyRSpec(rspec)
475 self.setStatus("Sending slice data (RSpec). This will take some time...")
478 dlg = RenewWindow(parent=self)
482 if not config.getSlice():
483 self.setStatus("<font color='red'>Slice not set yet!</font>")
486 if self.process.isRunning():
487 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
490 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
491 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
493 self.process.retrieveRspec()
494 self.setStatus("Refreshing slice data. This will take some time...")
496 def updateView(self):
497 global already_in_nodes
498 already_in_nodes = []
499 self.network_names = []
500 self.nodeModel.clear()
502 rspec = self.readSliceRSpec()
506 rootItem = self.nodeModel.invisibleRootItem()
507 #networks = sorted(rspec.get_network_list())
508 networks = rspec.get_networks()
509 for network in networks:
510 self.network_names.append(network)
512 #all_nodes = rspec.get_node_list(network)
513 #sliver_nodes = rspec.get_sliver_list(network)
514 all_nodes = rspec.get_nodes(network)
515 sliver_nodes = rspec.get_nodes_with_slivers(network)
516 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
518 networkItem = QStandardItem(QString(network))
519 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
520 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
522 already_in_nodes += sliver_nodes
524 # Add default slice tags
525 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
526 statusItem = QStandardItem(QString(""))
527 nodeStatus = QStandardItem(QString(""))
528 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
529 attrs = rspec.get_default_sliver_attributes(network)
530 for (name, value) in attrs:
531 tagstring = QString("%s: %s" % (name, value))
532 tagItem = QStandardItem(tagstring)
533 status = QStandardItem(QString(tag_status['in']))
534 nodeStatus = QStandardItem(QString(""))
535 nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
537 for node in sliver_nodes:
538 nodeItem = QStandardItem(QString(node))
539 statusItem = QStandardItem(QString(node_status['in']))
540 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
541 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
543 attrs = rspec.get_sliver_attributes(node, network)
544 for (name, value) in attrs:
545 tagstring = QString("%s: %s" % (name, value))
546 tagItem = QStandardItem(tagstring)
547 statusItem = QStandardItem(QString(tag_status['in']))
548 nodeStatus = QStandardItem(QString(""))
549 nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
551 for node in available_nodes:
552 nodeItem = QStandardItem(QString(node))
553 statusItem = QStandardItem(QString(node_status['out']))
554 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
555 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
557 self.filterModel.setSourceModel(self.nodeModel)
558 self.filterModel.setDynamicSortFilter(True)
560 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
561 self.nodeModel.setHorizontalHeaderLabels(headers)
563 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
564 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
565 self.nodeView.setModel(self.filterModel)
566 self.nodeView.hideColumn(KIND_COLUMN)
567 self.nodeView.expandAll()
568 self.nodeView.resizeColumnToContents(0)
569 self.nodeView.collapseAll()
571 def updateSliceName(self):
572 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
574 def nodeSelectionChanged(self, hostname):
575 self.parent().nodeSelectionChanged(hostname)
577 class MainScreen(SfaScreen):
578 def __init__(self, parent):
579 SfaScreen.__init__(self, parent)
581 slice = SliceWidget(self)
582 self.init(slice, "Nodes", "OneLab SFA crawler")
584 def rspecUpdated(self):
585 self.mainwin.rspecWindow.updateView()
587 def configurationChanged(self):
588 self.widget.updateSliceName()
589 self.widget.updateView()
590 self.mainwin.rspecWindow.updateView()
592 def nodeSelectionChanged(self, hostname):
593 self.mainwin.nodeSelectionChanged(hostname)