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
35 if index.parent().parent().isValid():
41 class NodeView(QTreeView):
42 def __init__(self, parent):
43 QTreeView.__init__(self, parent)
45 self.setAnimated(True)
46 self.setItemsExpandable(True)
47 self.setRootIsDecorated(True)
48 self.setAlternatingRowColors(True)
49 # self.setSelectionMode(self.MultiSelection)
50 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
51 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
52 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
54 def mouseDoubleClickEvent(self, event):
55 index = self.currentIndex()
57 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
58 status_data = status_index.data().toString()
59 node_index = model.index(index.row(), NAME_COLUMN, index.parent())
60 node_data = node_index.data().toString()
62 if itemType(node_index) == "tag":
63 data = node_index.data().toString()
64 tagname, value = data.split(": ")
65 if tagname not in settable_tags:
67 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
69 if status_data == tag_status['in']:
70 model.setData(status_index, QString(tag_status['remove']))
71 elif status_data == tag_status['add']:
72 model.setData(status_index, QString(tag_status['out']))
73 elif status_data == tag_status['remove']:
74 model.setData(status_index, QString(tag_status['in']))
75 else: model.setData(status_index, QString(node_status['out']))
78 if status_data == node_status['in']:
79 model.setData(status_index, QString(node_status['remove']))
80 elif status_data == node_status['out']:
81 model.setData(status_index, QString(node_status['add']))
82 elif status_data in (node_status['add'], node_status['remove']):
83 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
84 else: model.setData(status_index, QString(node_status['out']))
86 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
88 def mousePressEvent(self, event):
89 QTreeView.mousePressEvent(self, event)
90 if event.button() == Qt.LeftButton:
94 index = self.currentIndex()
96 status_index = model.index(index.row(), 1, index.parent())
97 status_data = status_index.data().toString()
98 node_index = model.index(index.row(), 0, index.parent())
99 node_data = node_index.data().toString()
101 if itemType(node_index) == "node":
103 if status_data in (node_status['in'], node_status['add'], ""):
104 # Pop up a dialog box for adding a new attribute
105 tagname, ok = QInputDialog.getItem(self, "Add tag",
106 "Tag name:", settable_tags)
108 value, ok = QInputDialog.getText(self, "Add tag",
109 "Value for tag '%s'" % tagname)
111 # Add a new row to the model for the tag
113 # For testing with the QStandardItemModel
114 #nodeItem = model.itemFromIndex(index)
115 #tagstring = QString("%s: %s" % (tagname, value))
116 #tagItem = QStandardItem(tagstring)
117 #status = QStandardItem(QString(tag_status['add']))
118 #nodeItem.appendRow([tagItem, status])
120 # We're using the QSortFilterProxyModel here
121 src_index = model.mapToSource(index)
122 src_model = src_index.model()
123 nodeItem = src_model.itemFromIndex(src_index)
124 tagstring = QString("%s: %s" % (tagname, value))
125 tagItem = QStandardItem(tagstring)
126 status = QStandardItem(QString(tag_status['add']))
127 nodeItem.appendRow([tagItem, status])
129 elif status_data in (node_status['out'], node_status['remove']):
130 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
133 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
135 def currentChanged(self, current, previous):
136 model = current.model()
137 node_index = model.index(current.row(), 0, current.parent())
138 node_data = node_index.data().toString()
139 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
143 class NodeNameDelegate(QStyledItemDelegate):
144 def __init__(self, parent):
145 QStyledItemDelegate.__init__(self, parent)
147 def paint(self, painter, option, index):
148 model = index.model()
149 status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
150 status_data = status_index.data().toString()
152 fm = QFontMetrics(option.font)
153 rect = QRect(option.rect)
155 data = index.data().toString()
156 rect.setHeight(rect.height() - 2)
157 rect.setWidth(fm.width(QString(data)) + 6)
158 rect.setX(rect.x() + 5)
159 rect.setY(rect.y() - 1)
161 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
163 path = QPainterPath()
164 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
167 painter.setRenderHint(QPainter.Antialiasing)
169 if option.state & QStyle.State_Selected:
170 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
172 if itemType(index) == "node":
173 if status_data == node_status['in']: # already in the slice
174 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
175 painter.setPen(QColor.fromRgb(0, 0, 0))
176 painter.drawText(rect, 0, QString(data))
178 elif status_data == node_status['add']: # newly added to the slice
179 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
180 painter.setPen(QColor.fromRgb(0, 0, 0))
181 painter.drawText(rect, 0, QString(data))
183 elif status_data == node_status['remove']: # removed from the slice
184 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
185 painter.setPen(QColor.fromRgb(0, 0, 0))
186 painter.drawText(rect, 0, QString(data))
189 painter.setPen(QColor.fromRgb(0, 0, 0))
190 painter.drawText(rect, 0, QString(data))
193 if status_data == tag_status['in']: # already in the slice
194 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
195 painter.setPen(QColor.fromRgb(0, 0, 0))
196 painter.drawText(rect, 0, QString(data))
198 elif status_data == tag_status['add']: # newly added to the slice
199 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
200 painter.setPen(QColor.fromRgb(0, 0, 0))
201 painter.drawText(rect, 0, QString(data))
203 elif status_data == tag_status['remove']: # removed from the slice
204 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
205 painter.setPen(QColor.fromRgb(0, 0, 0))
206 painter.drawText(rect, 0, QString(data))
209 painter.setPen(QColor.fromRgb(0, 0, 0))
210 painter.drawText(rect, 0, QString(data))
214 class NodeStatusDelegate(QStyledItemDelegate):
215 def __init__(self, parent):
216 QStyledItemDelegate.__init__(self, parent)
218 def paint(self, painter, option, index):
219 model = index.model()
220 nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
221 nodestatus_data = nodestatus_index.data().toString()
223 fm = QFontMetrics(option.font)
224 rect = QRect(option.rect)
226 data = index.data().toString()
227 rect.setHeight(rect.height() - 2)
228 rect.setWidth(fm.width(QString(data)) + 6)
229 rect.setX(rect.x() + 5)
230 rect.setY(rect.y() - 1)
232 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
234 path = QPainterPath()
235 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
238 painter.setRenderHint(QPainter.Antialiasing)
240 if option.state & QStyle.State_Selected:
241 painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
243 if (nodestatus_data == ""):
244 painter.setPen(QColor.fromRgb(0, 0, 0))
245 painter.drawText(rect, 0, QString(data))
246 elif (nodestatus_data == "boot"):
247 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
248 painter.setPen(QColor.fromRgb(0, 0, 0))
249 painter.drawText(rect, 0, QString(data))
251 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
252 painter.setPen(QColor.fromRgb(0, 0, 0))
253 painter.drawText(rect, 0, QString(data))
257 class NodeFilterProxyModel(QSortFilterProxyModel):
258 def __init__(self, parent=None):
259 QSortFilterProxyModel.__init__(self, parent)
260 self.hostname_filter_regex = None
261 self.nodestatus_filter = None
263 def setHostNameFilter(self, hostname):
264 self.hostname_filter_regex = QRegExp(hostname)
265 self.invalidateFilter()
267 def setNodeStatusFilter(self, status):
268 if (status == "all"):
269 self.nodestatus_filter = None
271 self.nodestatus_filter = status
272 self.invalidateFilter()
274 def filterAcceptsRow(self, sourceRow, source_parent):
275 kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
276 if (kind_data == "node"):
277 if self.hostname_filter_regex:
278 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
279 if (self.hostname_filter_regex.indexIn(name_data) < 0):
281 if self.nodestatus_filter:
282 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
283 if (nodestatus_data != self.nodestatus_filter):
287 class SliceWidget(QWidget):
288 def __init__(self, parent):
289 QWidget.__init__(self, parent)
291 self.network_names = []
292 self.process = SfiProcess(self)
294 self.slicename = QLabel("", self)
295 self.updateSliceName()
296 self.slicename.setScaledContents(False)
297 filterlabel = QLabel ("Filter: ", self)
298 filterbox = QComboBox(self)
299 filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
300 searchlabel = QLabel ("Search: ", self)
301 searchlabel.setScaledContents(False)
302 searchbox = QLineEdit(self)
303 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
305 toplayout = QHBoxLayout()
306 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
307 toplayout.addStretch()
308 toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
309 toplayout.addWidget(filterbox, 0, Qt.AlignRight)
310 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
311 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
313 self.nodeView = NodeView(self)
314 self.nodeModel = QStandardItemModel(0, 4, self)
315 self.filterModel = NodeFilterProxyModel(self)
317 self.nodeNameDelegate = NodeNameDelegate(self)
318 self.nodeStatusDelegate = NodeStatusDelegate(self)
320 refresh = QPushButton("Refresh Slice Data", self)
321 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
322 renew = QPushButton("Renew Slice", self)
323 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
324 submit = QPushButton("Submit", self)
325 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
327 bottomlayout = QHBoxLayout()
328 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
329 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
330 bottomlayout.addStretch()
331 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
333 layout = QVBoxLayout()
334 layout.addLayout(toplayout)
335 layout.addWidget(self.nodeView)
336 layout.addLayout(bottomlayout)
337 self.setLayout(layout)
338 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
340 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
341 self.connect(renew, SIGNAL('clicked()'), self.renew)
342 self.connect(submit, SIGNAL('clicked()'), self.submit)
343 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
344 self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
345 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
346 self.nodeSelectionChanged)
350 def submitFinished(self):
351 self.setStatus("<font color='green'>Slice data submitted.</font>")
352 QTimer.singleShot(1000, self.refresh)
354 def refreshFinished(self):
355 self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
357 self.parent().signalAll("rspecUpdated")
359 def readSliceRSpec(self):
360 rspec_file = config.getSliceRSpecFile()
361 if os.path.exists(rspec_file):
362 xml = open(rspec_file).read()
363 return parse_rspec(xml)
366 def setStatus(self, msg, timeout=None):
367 self.parent().setStatus(msg, timeout)
369 def checkRunningProcess(self):
370 if self.process.isRunning():
371 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
375 def search(self, search_string):
376 self.filterModel.setHostNameFilter(str(search_string))
378 def filter(self, filter_string):
379 self.filterModel.setNodeStatusFilter(str(filter_string))
381 def itemStatus(self, item):
382 statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
383 return statusItem.data(Qt.DisplayRole).toString()
385 def itemText(self, item):
386 return item.data(Qt.DisplayRole).toString()
388 # Recursively walk the tree, making changes to the RSpec
389 def process_subtree(self, rspec, item, depth = 0):
391 model = self.nodeModel
395 elif depth == 2: # Hostname
396 hostname = self.itemText(item)
397 testbed = self.itemText(item.parent())
398 status = self.itemStatus(item)
399 if status == node_status['add']:
400 print "Add hostname: %s" % hostname
401 rspec.add_slivers(str(hostname), testbed)
403 elif status == node_status['remove']:
404 print "Remove hostname: %s" % hostname
405 rspec.remove_slivers(str(hostname), testbed)
407 elif depth == 3: # Tag
408 tag, value = self.itemText(item).split(": ")
409 status = self.itemStatus(item)
410 tag = "%s" % tag # Prevent weird error from lxml
411 value = "%s" % value # Prevent weird error from lxml
412 node = self.itemText(item.parent())
413 testbed = self.itemText(item.parent().parent())
414 if status == tag_status['add']:
415 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
416 if node.startsWith(default_tags):
417 rspec.add_default_sliver_attribute(tag, value, testbed)
419 rspec.add_sliver_attribute(node, tag, value, testbed)
421 elif status == tag_status['remove']:
422 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
423 if node.startsWith(default_tags):
424 rspec.remove_default_sliver_attribute(tag, value, testbed)
426 rspec.remove_sliver_attribute(node, tag, value, testbed)
429 children = item.rowCount()
430 for row in range(0, children):
431 status = self.process_subtree(rspec, item.child(row), depth + 1)
432 change = change or status
437 if self.checkRunningProcess():
440 rspec = self.readSliceRSpec()
441 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
444 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
447 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
448 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
450 self.process.applyRSpec(rspec)
451 self.setStatus("Sending slice data (RSpec). This will take some time...")
454 dlg = RenewWindow(parent=self)
455 if (dlg.exec_() == QDialog.Accepted):
456 self.setStatus("Renewing Slice.")
458 self.renewProcess = SfiRenewer(config.getSlice(), dlg.get_new_expiration(), self)
459 self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
461 def renewFinished(self):
462 if self.renewProcess.statusMsg:
463 self.setStatus("Renew " + self.renewProcess.status + ": " + self.renewProcess.statusMsg)
465 self.setStatus("Renew " + self.renewProcess.status)
466 self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
467 self.renewProcess = None
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.getRSpecFromSM()
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 RenewWindow(QDialog):
566 def __init__(self, parent=None):
567 super(RenewWindow, self).__init__(parent)
568 self.setWindowTitle("Renew Slivers")
570 self.duration = QComboBox()
572 self.expirations = []
574 durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
576 now = datetime.datetime.utcnow()
577 for (weeks, desc) in durations:
578 exp = now + datetime.timedelta(days = weeks * 7)
579 desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
580 self.expirations.append(exp)
581 self.duration.addItem(desc)
583 self.duration.setCurrentIndex(0)
585 buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
586 buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
588 layout = QVBoxLayout()
589 layout.addWidget(self.duration)
590 layout.addWidget(buttonBox)
591 self.setLayout(layout)
593 self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
594 self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
599 def get_new_expiration(self):
600 index = self.duration.currentIndex()
601 return self.expirations[index]
603 class MainScreen(SfaScreen):
604 def __init__(self, parent):
605 SfaScreen.__init__(self, parent)
607 slice = SliceWidget(self)
608 self.init(slice, "Nodes", "OneLab SFA crawler")
610 def rspecUpdated(self):
611 self.mainwin.rspecWindow.updateView()
613 def configurationChanged(self):
614 self.widget.updateSliceName()
615 self.widget.updateView()
616 self.mainwin.rspecWindow.updateView()
618 def nodeSelectionChanged(self, hostname):
619 self.mainwin.nodeSelectionChanged(hostname)