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 if index.parent().parent().isValid():
36 class NodeView(QTreeView):
37 def __init__(self, parent):
38 QTreeView.__init__(self, parent)
40 self.setAnimated(True)
41 self.setItemsExpandable(True)
42 self.setRootIsDecorated(True)
43 self.setAlternatingRowColors(True)
44 # self.setSelectionMode(self.MultiSelection)
45 self.setAttribute(Qt.WA_MacShowFocusRect, 0)
46 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
47 self.setToolTip("Double click on a row to change its status. Right click on a host to add a tag.")
49 def mouseDoubleClickEvent(self, event):
50 index = self.currentIndex()
52 status_index = model.index(index.row(), 1, index.parent())
53 status_data = status_index.data().toString()
54 node_index = model.index(index.row(), 0, index.parent())
55 node_data = node_index.data().toString()
57 if itemType(node_index) == "tag":
58 data = node_index.data().toString()
59 tagname, value = data.split(": ")
60 if tagname not in settable_tags:
62 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
64 if status_data == tag_status['in']:
65 model.setData(status_index, QString(tag_status['remove']))
66 elif status_data == tag_status['add']:
67 model.setData(status_index, QString(tag_status['out']))
68 elif status_data == tag_status['remove']:
69 model.setData(status_index, QString(tag_status['in']))
70 else: model.setData(status_index, QString(node_status['out']))
73 if status_data == node_status['in']:
74 model.setData(status_index, QString(node_status['remove']))
75 elif status_data == node_status['out']:
76 model.setData(status_index, QString(node_status['add']))
77 elif status_data in (node_status['add'], node_status['remove']):
78 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
79 else: model.setData(status_index, QString(node_status['out']))
81 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
83 def mousePressEvent(self, event):
84 QTreeView.mousePressEvent(self, event)
85 if event.button() == Qt.LeftButton:
89 index = self.currentIndex()
91 status_index = model.index(index.row(), 1, index.parent())
92 status_data = status_index.data().toString()
93 node_index = model.index(index.row(), 0, index.parent())
94 node_data = node_index.data().toString()
96 if itemType(node_index) == "node":
98 if status_data in (node_status['in'], node_status['add'], ""):
99 # Pop up a dialog box for adding a new attribute
100 tagname, ok = QInputDialog.getItem(self, "Add tag",
101 "Tag name:", settable_tags)
103 value, ok = QInputDialog.getText(self, "Add tag",
104 "Value for tag '%s'" % tagname)
106 # Add a new row to the model for the tag
108 # For testing with the QStandardItemModel
109 #nodeItem = model.itemFromIndex(index)
110 #tagstring = QString("%s: %s" % (tagname, value))
111 #tagItem = QStandardItem(tagstring)
112 #status = QStandardItem(QString(tag_status['add']))
113 #nodeItem.appendRow([tagItem, status])
115 # We're using the QSortFilterProxyModel here
116 src_index = model.mapToSource(index)
117 src_model = src_index.model()
118 nodeItem = src_model.itemFromIndex(src_index)
119 tagstring = QString("%s: %s" % (tagname, value))
120 tagItem = QStandardItem(tagstring)
121 status = QStandardItem(QString(tag_status['add']))
122 nodeItem.appendRow([tagItem, status])
124 elif status_data in (node_status['out'], node_status['remove']):
125 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
128 model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
130 def currentChanged(self, current, previous):
131 model = current.model()
132 node_index = model.index(current.row(), 0, current.parent())
133 node_data = node_index.data().toString()
134 self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
138 class NodeNameDelegate(QStyledItemDelegate):
139 def __init__(self, parent):
140 QStyledItemDelegate.__init__(self, parent)
142 def paint(self, painter, option, index):
143 model = index.model()
144 status_index = model.index(index.row(), 1, index.parent())
145 status_data = status_index.data().toString()
147 fm = QFontMetrics(option.font)
150 data = index.data().toString()
151 rect.setHeight(rect.height() - 2)
152 rect.setWidth(fm.width(QString(data)) + 6)
153 rect.setX(rect.x() + 5)
154 rect.setY(rect.y() - 1)
156 x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
158 path = QPainterPath()
159 path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
162 painter.setRenderHint(QPainter.Antialiasing)
164 if itemType(index) == "node":
165 if status_data == node_status['in']: # already in the slice
166 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
167 painter.setPen(QColor.fromRgb(0, 0, 0))
168 painter.drawText(option.rect, 0, QString(data))
170 elif status_data == node_status['add']: # newly added to the slice
171 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
172 painter.setPen(QColor.fromRgb(0, 0, 0))
173 painter.drawText(option.rect, 0, QString(data))
175 elif status_data == node_status['remove']: # removed from the slice
176 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
177 painter.setPen(QColor.fromRgb(0, 0, 0))
178 painter.drawText(option.rect, 0, QString(data))
181 painter.setPen(QColor.fromRgb(0, 0, 0))
182 painter.drawText(option.rect, 0, QString(data))
185 if status_data == tag_status['in']: # already in the slice
186 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
187 painter.setPen(QColor.fromRgb(0, 0, 0))
188 painter.drawText(option.rect, 0, QString(data))
190 elif status_data == tag_status['add']: # newly added to the slice
191 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
192 painter.setPen(QColor.fromRgb(0, 0, 0))
193 painter.drawText(option.rect, 0, QString(data))
195 elif status_data == tag_status['remove']: # removed from the slice
196 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
197 painter.setPen(QColor.fromRgb(0, 0, 0))
198 painter.drawText(option.rect, 0, QString(data))
201 painter.setPen(QColor.fromRgb(0, 0, 0))
202 painter.drawText(option.rect, 0, QString(data))
206 class SliceWidget(QWidget):
207 def __init__(self, parent):
208 QWidget.__init__(self, parent)
210 self.network_names = []
211 self.process = SfiProcess(self)
213 self.slicename = QLabel("", self)
214 self.updateSliceName()
215 self.slicename.setScaledContents(False)
216 searchlabel = QLabel ("Search: ", self)
217 searchlabel.setScaledContents(False)
218 searchbox = QLineEdit(self)
219 searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
221 toplayout = QHBoxLayout()
222 toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
223 toplayout.addStretch()
224 toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
225 toplayout.addWidget(searchbox, 0, Qt.AlignRight)
227 self.nodeView = NodeView(self)
228 self.nodeModel = QStandardItemModel(0, 2, self)
229 self.filterModel = QSortFilterProxyModel(self) # enable filtering
231 self.nodeNameDelegate = NodeNameDelegate(self)
233 refresh = QPushButton("Update Slice Data", self)
234 refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
235 renew = QPushButton("Renew Slice", self)
236 renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
237 submit = QPushButton("Submit", self)
238 submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
240 bottomlayout = QHBoxLayout()
241 bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
242 bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
243 bottomlayout.addStretch()
244 bottomlayout.addWidget(submit, 0, Qt.AlignRight)
246 layout = QVBoxLayout()
247 layout.addLayout(toplayout)
248 layout.addWidget(self.nodeView)
249 layout.addLayout(bottomlayout)
250 self.setLayout(layout)
251 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
253 self.connect(refresh, SIGNAL('clicked()'), self.refresh)
254 self.connect(renew, SIGNAL('clicked()'), self.renew)
255 self.connect(submit, SIGNAL('clicked()'), self.submit)
256 self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
257 self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
258 self.nodeSelectionChanged)
262 def submitFinished(self):
263 self.setStatus("<font color='green'>Slice data submitted.</font>")
264 QTimer.singleShot(1000, self.refresh)
266 def refreshFinished(self):
267 self.setStatus("<font color='green'>Slice data updated.</font>", timeout=5000)
269 self.parent().signalAll("rspecUpdated")
271 def readSliceRSpec(self):
272 rspec_file = config.getSliceRSpecFile()
273 if os.path.exists(rspec_file):
274 xml = open(rspec_file).read()
275 return parse_rspec(xml)
278 def setStatus(self, msg, timeout=None):
279 self.parent().setStatus(msg, timeout)
281 def checkRunningProcess(self):
282 if self.process.isRunning():
283 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
287 def filter(self, filter_string):
288 # for hierarchical models QSortFilterProxyModel applies the
289 # sort recursively. if the parent doesn't match the criteria
290 # we won't be able to match the children. so we need to match
291 # parent (by matching the network_names)
292 networks = ["^%s$" % n for n in self.network_names]
293 filters = networks + [str(filter_string)]
294 self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
296 def itemStatus(self, item):
297 statusItem = item.parent().child(item.row(), 1)
298 return statusItem.data(Qt.DisplayRole).toString()
300 def itemText(self, item):
301 return item.data(Qt.DisplayRole).toString()
303 # Recursively walk the tree, making changes to the RSpec
304 def process_subtree(self, rspec, item, depth = 0):
306 model = self.nodeModel
310 elif depth == 2: # Hostname
311 hostname = self.itemText(item)
312 testbed = self.itemText(item.parent())
313 status = self.itemStatus(item)
314 if status == node_status['add']:
315 print "Add hostname: %s" % hostname
316 rspec.add_slivers(str(hostname), testbed)
318 elif status == node_status['remove']:
319 print "Remove hostname: %s" % hostname
320 rspec.remove_slivers(str(hostname), testbed)
322 elif depth == 3: # Tag
323 tag, value = self.itemText(item).split(": ")
324 status = self.itemStatus(item)
325 tag = "%s" % tag # Prevent weird error from lxml
326 value = "%s" % value # Prevent weird error from lxml
327 node = self.itemText(item.parent())
328 testbed = self.itemText(item.parent().parent())
329 if status == tag_status['add']:
330 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
331 if node.startsWith(default_tags):
332 rspec.add_default_sliver_attribute(tag, value, testbed)
334 rspec.add_sliver_attribute(node, tag, value, testbed)
336 elif status == tag_status['remove']:
337 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
338 if node.startsWith(default_tags):
339 rspec.remove_default_sliver_attribute(tag, value, testbed)
341 rspec.remove_sliver_attribute(node, tag, value, testbed)
344 children = item.rowCount()
345 for row in range(0, children):
346 status = self.process_subtree(rspec, item.child(row), depth + 1)
347 change = change or status
352 if self.checkRunningProcess():
355 rspec = self.readSliceRSpec()
356 change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
359 self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
362 self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
363 self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
365 self.process.applyRSpec(rspec)
366 self.setStatus("Sending slice data (RSpec). This will take some time...")
369 dlg = RenewWindow(parent=self)
370 if (dlg.exec_() == QDialog.Accepted):
371 self.setStatus("Renewing Slice.")
373 self.renewProcess = SfiRenewer(config.getSlice(), dlg.get_new_expiration(), self)
374 self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
376 def renewFinished(self):
377 if self.renewProcess.statusMsg:
378 self.setStatus("Renew " + self.renewProcess.status + ": " + self.renewProcess.statusMsg)
380 self.setStatus("Renew " + self.renewProcess.status)
381 self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
382 self.renewProcess = None
385 if not config.getSlice():
386 self.setStatus("<font color='red'>Slice not set yet!</font>")
389 if self.process.isRunning():
390 self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
393 self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
394 self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
396 self.process.getRSpecFromSM()
397 self.setStatus("Updating slice data. This will take some time...")
399 def updateView(self):
400 global already_in_nodes
401 already_in_nodes = []
402 self.network_names = []
403 self.nodeModel.clear()
405 rspec = self.readSliceRSpec()
409 rootItem = self.nodeModel.invisibleRootItem()
410 #networks = sorted(rspec.get_network_list())
411 networks = rspec.get_networks()
412 for network in networks:
413 self.network_names.append(network)
415 #all_nodes = rspec.get_node_list(network)
416 #sliver_nodes = rspec.get_sliver_list(network)
417 all_nodes = rspec.get_nodes(network)
418 sliver_nodes = rspec.get_nodes_with_slivers(network)
419 available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
421 networkItem = QStandardItem(QString(network))
422 msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
423 rootItem.appendRow([networkItem, QStandardItem(QString(msg))])
425 already_in_nodes += sliver_nodes
427 # Add default slice tags
428 nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
429 statusItem = QStandardItem(QString(""))
430 networkItem.appendRow([nodeItem, statusItem])
431 attrs = rspec.get_default_sliver_attributes(network)
432 for (name, value) in attrs:
433 tagstring = QString("%s: %s" % (name, value))
434 tagItem = QStandardItem(tagstring)
435 status = QStandardItem(QString(tag_status['in']))
436 nodeItem.appendRow([tagItem, status])
438 for node in sliver_nodes:
439 nodeItem = QStandardItem(QString(node))
440 statusItem = QStandardItem(QString(node_status['in']))
441 networkItem.appendRow([nodeItem, statusItem])
443 attrs = rspec.get_sliver_attributes(node, network)
444 for (name, value) in attrs:
445 tagstring = QString("%s: %s" % (name, value))
446 tagItem = QStandardItem(tagstring)
447 statusItem = QStandardItem(QString(tag_status['in']))
448 nodeItem.appendRow([tagItem, statusItem])
450 for node in available_nodes:
451 nodeItem = QStandardItem(QString(node))
452 statusItem = QStandardItem(QString(node_status['out']))
453 networkItem.appendRow([nodeItem, statusItem])
455 self.filterModel.setSourceModel(self.nodeModel)
456 self.filterModel.setFilterKeyColumn(-1)
457 self.filterModel.setDynamicSortFilter(True)
459 headers = QStringList() << "Hostname or Tag" << "Status"
460 self.nodeModel.setHorizontalHeaderLabels(headers)
462 self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
463 self.nodeView.setModel(self.filterModel)
464 self.nodeView.expandAll()
465 self.nodeView.resizeColumnToContents(0)
466 self.nodeView.collapseAll()
468 def updateSliceName(self):
469 self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
471 def nodeSelectionChanged(self, hostname):
472 self.parent().nodeSelectionChanged(hostname)
474 class RenewWindow(QDialog):
475 def __init__(self, parent=None):
476 super(RenewWindow, self).__init__(parent)
477 self.setWindowTitle("Renew Slivers")
479 self.duration = QComboBox()
481 self.expirations = []
483 durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
485 now = datetime.datetime.utcnow()
486 for (weeks, desc) in durations:
487 exp = now + datetime.timedelta(days = weeks * 7)
488 desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
489 self.expirations.append(exp)
490 self.duration.addItem(desc)
492 self.duration.setCurrentIndex(0)
494 buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
495 buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
497 layout = QVBoxLayout()
498 layout.addWidget(self.duration)
499 layout.addWidget(buttonBox)
500 self.setLayout(layout)
502 self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
503 self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
508 def get_new_expiration(self):
509 index = self.duration.currentIndex()
510 return self.expirations[index]
512 class MainScreen(SfaScreen):
513 def __init__(self, parent):
514 SfaScreen.__init__(self, parent)
516 slice = SliceWidget(self)
517 self.init(slice, "Main Window", "OneLab SFA crawler")
519 def rspecUpdated(self):
520 self.mainwin.rspecWindow.updateView()
522 def configurationChanged(self):
523 self.widget.updateSliceName()
524 self.widget.updateView()
525 self.mainwin.rspecWindow.updateView()
527 def nodeSelectionChanged(self, hostname):
528 self.mainwin.nodeSelectionChanged(hostname)