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 SfiRenewer
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 if index.parent().parent().isValid():
40 class NodeView(QTreeView):
41 def __init__(self, parent):
42 QTreeView.__init__(self, parent)
44 self.setAnimated(True)
45 self.setItemsExpandable(True)
46 self.setRootIsDecorated(True)
47 self.setAlternatingRowColors(True)
48 # self.setSelectionMode(self.MultiSelection)
49 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
50 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
51 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
53 def mouseDoubleClickEvent(self, event):
54 index = self.currentIndex()
56 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
57 status_data = status_index.data().toString()
58 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
59 node_data = node_index.data().toString()
61 if itemType(node_index) == "tag":
62 data = node_index.data().toString()
63 tagname, value = data.split(": ")
64 if tagname not in settable_tags:
66 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
68 if status_data == tag_status['in']:
69 model.setData(status_index, QString(tag_status['remove']))
70 elif status_data == tag_status['add']:
71 model.setData(status_index, QString(tag_status['out']))
72 elif status_data == tag_status['remove']:
73 model.setData(status_index, QString(tag_status['in']))
74 else: model.setData(status_index, QString(node_status['out']))
77 if status_data == node_status['in']:
78 model.setData(status_index, QString(node_status['remove']))
79 elif status_data == node_status['out']:
80 model.setData(status_index, QString(node_status['add']))
81 elif status_data in (node_status['add'], node_status['remove']):
82 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
83 else: model.setData(status_index, QString(node_status['out']))
85 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
87 def mousePressEvent(self, event):
88 QTreeView.mousePressEvent(self, event)
89 if event.button() == Qt.LeftButton:
93 index = self.currentIndex()
95 status_index = model.index(index.row(), 1, index.parent())
96 status_data = status_index.data().toString()
97 node_index = model.index(index.row(), 0, index.parent())
98 node_data = node_index.data().toString()
100 if itemType(node_index) == "node":
102 if status_data in (node_status['in'], node_status['add'], ""):
103 # Pop up a dialog box for adding a new attribute
104 tagname, ok = QInputDialog.getItem(self, "Add tag",
105 "Tag name:", settable_tags)
107 value, ok = QInputDialog.getText(self, "Add tag",
108 "Value for tag '%s'" % tagname)
110 # Add a new row to the model for the tag
112 # For testing with the QStandardItemModel
113 #nodeItem = model.itemFromIndex(index)
114 #tagstring = QString("%s: %s" % (tagname, value))
115 #tagItem = QStandardItem(tagstring)
116 #status = QStandardItem(QString(tag_status['add']))
117 #nodeItem.appendRow([tagItem, status])
119 # We're using the QSortFilterProxyModel here
120 src_index = model.mapToSource(index)
121 src_model = src_index.model()
122 nodeItem = src_model.itemFromIndex(src_index)
123 tagstring = QString("%s: %s" % (tagname, value))
124 tagItem = QStandardItem(tagstring)
125 status = QStandardItem(QString(tag_status['add']))
126 nodeItem.appendRow([tagItem, status])
128 elif status_data in (node_status['out'], node_status['remove']):
129 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
132 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
134 def currentChanged(self, current, previous):
135 model = current.model()
136 node_index = model.index(current.row(), 0, current.parent())
137 node_data = node_index.data().toString()
138 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
142 class NodeNameDelegate(QStyledItemDelegate):
143 def __init__(self, parent):
144 QStyledItemDelegate.__init__(self, parent)
146 def paint(self, painter, option, index):
147 model = index.model()
148 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
149 status_data = status_index.data().toString()
151 fm = QFontMetrics(option.font)
152 rect = QRect(option.rect)
154 data = index.data().toString()
155 rect.setHeight(rect.height() - 2)
156 rect.setWidth(fm.width(QString(data)) + 6)
157 rect.setX(rect.x() + 5)
158 rect.setY(rect.y() - 1)
160 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
162 path = QPainterPath()
163 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
166 painter.setRenderHint(QPainter.Antialiasing)
168 if option.state & QStyle.State_Selected:
169 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
171 if itemType(index) == "node":
172 if status_data == node_status['in']: # already in the slice
173 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
174 painter.setPen(QColor.fromRgb(0, 0, 0))
175 painter.drawText(rect, 0, QString(data))
177 elif status_data == node_status['add']: # newly added to the slice
178 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
179 painter.setPen(QColor.fromRgb(0, 0, 0))
180 painter.drawText(rect, 0, QString(data))
182 elif status_data == node_status['remove']: # removed from the slice
183 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
184 painter.setPen(QColor.fromRgb(0, 0, 0))
185 painter.drawText(rect, 0, QString(data))
188 painter.setPen(QColor.fromRgb(0, 0, 0))
189 painter.drawText(rect, 0, QString(data))
192 if status_data == tag_status['in']: # already in the slice
193 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
194 painter.setPen(QColor.fromRgb(0, 0, 0))
195 painter.drawText(rect, 0, QString(data))
197 elif status_data == tag_status['add']: # newly added to the slice
198 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
199 painter.setPen(QColor.fromRgb(0, 0, 0))
200 painter.drawText(rect, 0, QString(data))
202 elif status_data == tag_status['remove']: # removed from the slice
203 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
204 painter.setPen(QColor.fromRgb(0, 0, 0))
205 painter.drawText(rect, 0, QString(data))
208 painter.setPen(QColor.fromRgb(0, 0, 0))
209 painter.drawText(rect, 0, QString(data))
213 class NodeStatusDelegate(QStyledItemDelegate):
214 def __init__(self, parent):
215 QStyledItemDelegate.__init__(self, parent)
217 def paint(self, painter, option, index):
218 model = index.model()
219 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
220 nodestatus_data = nodestatus_index.data().toString()
222 fm = QFontMetrics(option.font)
223 rect = QRect(option.rect)
225 data = index.data().toString()
226 rect.setHeight(rect.height() - 2)
227 rect.setWidth(fm.width(QString(data)) + 6)
228 rect.setX(rect.x() + 5)
229 rect.setY(rect.y() - 1)
231 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
233 path = QPainterPath()
234 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
237 painter.setRenderHint(QPainter.Antialiasing)
239 if option.state & QStyle.State_Selected:
240 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
242 if (nodestatus_data == ""):
243 painter.setPen(QColor.fromRgb(0, 0, 0))
244 painter.drawText(rect, 0, QString(data))
245 elif (nodestatus_data == "boot"):
246 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
247 painter.setPen(QColor.fromRgb(0, 0, 0))
248 painter.drawText(rect, 0, QString(data))
250 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
251 painter.setPen(QColor.fromRgb(0, 0, 0))
252 painter.drawText(rect, 0, QString(data))
256 class SliceWidget(QWidget):
257 def __init__(self, parent):
258 QWidget.__init__(self, parent)
260 self.network_names = []
261 self.process = SfiProcess(self)
263 self.slicename = QLabel("", self)
264 self.updateSliceName()
265 self.slicename.setScaledContents(False)
266 searchlabel = QLabel ("Search: ", self)
267 searchlabel.setScaledContents(False)
268 searchbox = QLineEdit(self)
269 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
271 toplayout = QHBoxLayout()
272 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
273 toplayout.addStretch()
274 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
275 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
277 self.nodeView = NodeView(self)
278 self.nodeModel = QStandardItemModel(0, 2, self)
279 self.filterModel = QSortFilterProxyModel(self) # enable filtering
281 self.nodeNameDelegate = NodeNameDelegate(self)
282 self.nodeStatusDelegate = NodeStatusDelegate(self)
284 refresh = QPushButton("Refresh Slice Data", self)
285 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
286 renew = QPushButton("Renew Slice", self)
287 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
288 submit = QPushButton("Submit", self)
289 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
291 bottomlayout = QHBoxLayout()
292 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
293 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
294 bottomlayout.addStretch()
295 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
297 layout = QVBoxLayout()
298 layout.addLayout(toplayout)
299 layout.addWidget(self.nodeView)
300 layout.addLayout(bottomlayout)
301 self.setLayout(layout)
302 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
304 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
305 self.connect(renew, SIGNAL('clicked()'), self.renew)
306 self.connect(submit, SIGNAL('clicked()'), self.submit)
307 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
308 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
309 self.nodeSelectionChanged)
313 def submitFinished(self):
314 self.setStatus("<font color='green'>Slice data submitted.</font>")
315 QTimer.singleShot(1000, self.refresh)
317 def refreshFinished(self):
318 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
320 self.parent().signalAll("rspecUpdated")
322 def readSliceRSpec(self):
323 rspec_file = config.getSliceRSpecFile()
324 if os.path.exists(rspec_file):
325 xml = open(rspec_file).read()
326 return parse_rspec(xml)
329 def setStatus(self, msg, timeout=None):
330 self.parent().setStatus(msg, timeout)
332 def checkRunningProcess(self):
333 if self.process.isRunning():
334 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
338 def filter(self, filter_string):
339 # for hierarchical models QSortFilterProxyModel applies the
340 # sort recursively. if the parent doesn't match the criteria
341 # we won't be able to match the children. so we need to match
342 # parent (by matching the network_names)
343 networks = ["^%s$" % n for n in self.network_names]
344 filters = networks + [str(filter_string)]
345 self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
347 def itemStatus(self, item):
348 statusItem = item.parent().child(item.row(), 1)
349 return statusItem.data(Qt.DisplayRole).toString()
351 def itemText(self, item):
352 return item.data(Qt.DisplayRole).toString()
354 # Recursively walk the tree, making changes to the RSpec
355 def process_subtree(self, rspec, item, depth = 0):
357 model = self.nodeModel
361 elif depth == 2: # Hostname
362 hostname = self.itemText(item)
363 testbed = self.itemText(item.parent())
364 status = self.itemStatus(item)
365 if status == node_status['add']:
366 print "Add hostname: %s" % hostname
367 rspec.add_slivers(str(hostname), testbed)
369 elif status == node_status['remove']:
370 print "Remove hostname: %s" % hostname
371 rspec.remove_slivers(str(hostname), testbed)
373 elif depth == 3: # Tag
374 tag, value = self.itemText(item).split(": ")
375 status = self.itemStatus(item)
376 tag = "%s" % tag # Prevent weird error from lxml
377 value = "%s" % value # Prevent weird error from lxml
378 node = self.itemText(item.parent())
379 testbed = self.itemText(item.parent().parent())
380 if status == tag_status['add']:
381 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
382 if node.startsWith(default_tags):
383 rspec.add_default_sliver_attribute(tag, value, testbed)
385 rspec.add_sliver_attribute(node, tag, value, testbed)
387 elif status == tag_status['remove']:
388 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
389 if node.startsWith(default_tags):
390 rspec.remove_default_sliver_attribute(tag, value, testbed)
392 rspec.remove_sliver_attribute(node, tag, value, testbed)
395 children = item.rowCount()
396 for row in range(0, children):
397 status = self.process_subtree(rspec, item.child(row), depth + 1)
398 change = change or status
403 if self.checkRunningProcess():
406 rspec = self.readSliceRSpec()
407 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
410 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
413 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
414 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
416 self.process.applyRSpec(rspec)
417 self.setStatus("Sending slice data (RSpec). This will take some time...")
420 dlg = RenewWindow(parent=self)
421 if (dlg.exec_() == QDialog.Accepted):
422 self.setStatus("Renewing Slice.")
424 self.renewProcess = SfiRenewer(config.getSlice(), dlg.get_new_expiration(), self)
425 self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
427 def renewFinished(self):
428 if self.renewProcess.statusMsg:
429 self.setStatus("Renew " + self.renewProcess.status + ": " + self.renewProcess.statusMsg)
431 self.setStatus("Renew " + self.renewProcess.status)
432 self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
433 self.renewProcess = None
436 if not config.getSlice():
437 self.setStatus("<font color='red'>Slice not set yet!</font>")
440 if self.process.isRunning():
441 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
444 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
445 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
447 self.process.getRSpecFromSM()
448 self.setStatus("Refreshing slice data. This will take some time...")
450 def updateView(self):
451 global already_in_nodes
452 already_in_nodes = []
453 self.network_names = []
454 self.nodeModel.clear()
456 rspec = self.readSliceRSpec()
460 rootItem = self.nodeModel.invisibleRootItem()
461 #networks = sorted(rspec.get_network_list())
462 networks = rspec.get_networks()
463 for network in networks:
464 self.network_names.append(network)
466 #all_nodes = rspec.get_node_list(network)
467 #sliver_nodes = rspec.get_sliver_list(network)
468 all_nodes = rspec.get_nodes(network)
469 sliver_nodes = rspec.get_nodes_with_slivers(network)
470 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
472 networkItem = QStandardItem(QString(network))
473 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
474 rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg))])
476 already_in_nodes += sliver_nodes
478 # Add default slice tags
479 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
480 statusItem = QStandardItem(QString(""))
481 networkItem.appendRow([nodeItem, statusItem])
482 attrs = rspec.get_default_sliver_attributes(network)
483 for (name, value) in attrs:
484 tagstring = QString("%s: %s" % (name, value))
485 tagItem = QStandardItem(tagstring)
486 status = QStandardItem(QString(tag_status['in']))
487 nodeStatus = QStandardItem(QString(""))
488 nodeItem.appendRow([tagItem, nodeStatus, status])
490 for node in sliver_nodes:
491 nodeItem = QStandardItem(QString(node))
492 statusItem = QStandardItem(QString(node_status['in']))
493 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
494 networkItem.appendRow([nodeItem, nodeStatus, statusItem])
496 attrs = rspec.get_sliver_attributes(node, network)
497 for (name, value) in attrs:
498 tagstring = QString("%s: %s" % (name, value))
499 tagItem = QStandardItem(tagstring)
500 statusItem = QStandardItem(QString(tag_status['in']))
501 nodeStatus = QStandardItem(QString(""))
502 nodeItem.appendRow([tagItem, nodeStatus, statusItem])
504 for node in available_nodes:
505 nodeItem = QStandardItem(QString(node))
506 statusItem = QStandardItem(QString(node_status['out']))
507 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
508 networkItem.appendRow([nodeItem, nodeStatus, statusItem])
510 self.filterModel.setSourceModel(self.nodeModel)
511 self.filterModel.setFilterKeyColumn(-1)
512 self.filterModel.setDynamicSortFilter(True)
514 headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status"
515 self.nodeModel.setHorizontalHeaderLabels(headers)
517 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
518 self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
519 self.nodeView.setModel(self.filterModel)
520 self.nodeView.expandAll()
521 self.nodeView.resizeColumnToContents(0)
522 self.nodeView.collapseAll()
524 def updateSliceName(self):
525 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
527 def nodeSelectionChanged(self, hostname):
528 self.parent().nodeSelectionChanged(hostname)
530 class RenewWindow(QDialog):
531 def __init__(self, parent=None):
532 super(RenewWindow, self).__init__(parent)
533 self.setWindowTitle("Renew Slivers")
535 self.duration = QComboBox()
537 self.expirations = []
539 durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
541 now = datetime.datetime.utcnow()
542 for (weeks, desc) in durations:
543 exp = now + datetime.timedelta(days = weeks * 7)
544 desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
545 self.expirations.append(exp)
546 self.duration.addItem(desc)
548 self.duration.setCurrentIndex(0)
550 buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
551 buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
553 layout = QVBoxLayout()
554 layout.addWidget(self.duration)
555 layout.addWidget(buttonBox)
556 self.setLayout(layout)
558 self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
559 self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
564 def get_new_expiration(self):
565 index = self.duration.currentIndex()
566 return self.expirations[index]
568 class MainScreen(SfaScreen):
569 def __init__(self, parent):
570 SfaScreen.__init__(self, parent)
572 slice = SliceWidget(self)
573 self.init(slice, "Nodes", "OneLab SFA crawler")
575 def rspecUpdated(self):
576 self.mainwin.rspecWindow.updateView()
578 def configurationChanged(self):
579 self.widget.updateSliceName()
580 self.widget.updateView()
581 self.mainwin.rspecWindow.updateView()
583 def nodeSelectionChanged(self, hostname):
584 self.mainwin.nodeSelectionChanged(hostname)